SubtypeSwitcher.java revision fefda4e6df5c2f8e2b2730dfe5b88644a1caaa6b
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 final 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        // TODO: Update an icon for shortcut IME
167        Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts =
168                mImm.getShortcutInputMethodsAndSubtypes();
169        for (InputMethodInfo imi: shortcuts.keySet()) {
170            List<InputMethodSubtype> subtypes = shortcuts.get(imi);
171            // TODO: Returns the first found IMI for now. Should handle all shortcuts as
172            // appropriate.
173            mShortcutInfo = imi;
174            // TODO: Pick up the first found subtype for now. Should handle all subtypes
175            // as appropriate.
176            mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null;
177            break;
178        }
179    }
180
181    // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function.
182    public void updateSubtype(InputMethodSubtype newSubtype) {
183        final String newLocale;
184        final String newMode;
185        if (newSubtype == null) {
186            // Normally, newSubtype shouldn't be null. But just in case newSubtype was null,
187            // fallback to the default locale and mode.
188            Log.w(TAG, "Couldn't get the current subtype.");
189            newLocale = "en_US";
190            newMode = KEYBOARD_MODE;
191        } else {
192            newLocale = newSubtype.getLocale();
193            newMode = newSubtype.getMode();
194        }
195        if (DBG) {
196            Log.w(TAG, "Update subtype to:" + newLocale + "," + newMode
197                    + ", from: " + mInputLocaleStr + ", " + mMode);
198        }
199        boolean languageChanged = false;
200        if (!newLocale.equals(mInputLocaleStr)) {
201            if (mInputLocaleStr != null) {
202                languageChanged = true;
203            }
204            updateInputLocale(newLocale);
205        }
206        boolean modeChanged = false;
207        String oldMode = mMode;
208        if (!newMode.equals(mMode)) {
209            if (mMode != null) {
210                modeChanged = true;
211            }
212            mMode = newMode;
213        }
214        if (isKeyboardMode()) {
215            if (modeChanged) {
216                if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) {
217                    mVoiceInput.cancel();
218                }
219            }
220            if (modeChanged || languageChanged) {
221                mService.onRefreshKeyboard();
222            }
223        } else if (isVoiceMode()) {
224            // If needsToShowWarningDialog is true, voice input need to show warning before
225            // show recognition view.
226            if (languageChanged || modeChanged
227                    || VoiceIMEConnector.getInstance().needsToShowWarningDialog()) {
228                if (mVoiceInput != null) {
229                    triggerVoiceIME();
230                }
231            }
232        } else {
233            Log.w(TAG, "Unknown subtype mode: " + mMode);
234        }
235    }
236
237    // Update the current input locale from Locale string.
238    private void updateInputLocale(String inputLocaleStr) {
239        // example: inputLocaleStr = "en_US" "en" ""
240        // "en_US" --> language: en  & country: US
241        // "en" --> language: en
242        // "" --> the system locale
243        mLocaleSplitter.setString(inputLocaleStr);
244        if (mLocaleSplitter.hasNext()) {
245            String language = mLocaleSplitter.next();
246            if (mLocaleSplitter.hasNext()) {
247                mInputLocale = new Locale(language, mLocaleSplitter.next());
248            } else {
249                mInputLocale = new Locale(language);
250            }
251            mInputLocaleStr = inputLocaleStr;
252        } else {
253            mInputLocale = mSystemLocale;
254            String country = mSystemLocale.getCountry();
255            mInputLocaleStr = mSystemLocale.getLanguage()
256                    + (TextUtils.isEmpty(country) ? "" : "_" + mSystemLocale.getLanguage());
257        }
258        mIsSystemLanguageSameAsInputLanguage = getSystemLocale().getLanguage().equalsIgnoreCase(
259                getInputLocale().getLanguage());
260        mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1
261                && mIsSystemLanguageSameAsInputLanguage);
262    }
263
264    ////////////////////////////
265    // Shortcut IME functions //
266    ////////////////////////////
267
268    public void switchToShortcutIME() {
269        IBinder token = mService.getWindow().getWindow().getAttributes().token;
270        if (token == null || mShortcutInfo == null) {
271            return;
272        }
273        mImm.setInputMethodAndSubtype(token, mShortcutInfo.getId(), mShortcutSubtype);
274    }
275
276    public Drawable getShortcutIcon() {
277        return getSubtypeIcon(mShortcutInfo, mShortcutSubtype);
278    }
279
280    private Drawable getSubtypeIcon(InputMethodInfo imi, InputMethodSubtype subtype) {
281        final PackageManager pm = mService.getPackageManager();
282        if (imi != null) {
283            final String imiPackageName = imi.getPackageName();
284            if (DBG) {
285                Log.d(TAG, "Update icons of IME: " + imiPackageName + ","
286                        + subtype.getLocale() + "," + subtype.getMode());
287            }
288            if (subtype != null) {
289                return pm.getDrawable(imiPackageName, subtype.getIconResId(),
290                        imi.getServiceInfo().applicationInfo);
291            } else if (imi.getSubtypeCount() > 0 && imi.getSubtypeAt(0) != null) {
292                return pm.getDrawable(imiPackageName,
293                        imi.getSubtypeAt(0).getIconResId(),
294                        imi.getServiceInfo().applicationInfo);
295            } else {
296                try {
297                    return pm.getApplicationInfo(imiPackageName, 0).loadIcon(pm);
298                } catch (PackageManager.NameNotFoundException e) {
299                    Log.w(TAG, "IME can't be found: " + imiPackageName);
300                }
301            }
302        }
303        return null;
304    }
305
306    //////////////////////////////////
307    // Language Switching functions //
308    //////////////////////////////////
309
310    public int getEnabledKeyboardLocaleCount() {
311        if (mConfigUseSpacebarLanguageSwitcher) {
312            return mLanguageSwitcher.getLocaleCount();
313        } else {
314            return mEnabledKeyboardSubtypesOfCurrentInputMethod.size();
315        }
316    }
317
318    public boolean useSpacebarLanguageSwitcher() {
319        return mConfigUseSpacebarLanguageSwitcher;
320    }
321
322    public boolean needsToDisplayLanguage() {
323        return mNeedsToDisplayLanguage;
324    }
325
326    public Locale getInputLocale() {
327        if (mConfigUseSpacebarLanguageSwitcher) {
328            return mLanguageSwitcher.getInputLocale();
329        } else {
330            return mInputLocale;
331        }
332    }
333
334    public String getInputLocaleStr() {
335        if (mConfigUseSpacebarLanguageSwitcher) {
336            String inputLanguage = null;
337            inputLanguage = mLanguageSwitcher.getInputLanguage();
338            // Should return system locale if there is no Language available.
339            if (inputLanguage == null) {
340                inputLanguage = getSystemLocale().getLanguage();
341            }
342            return inputLanguage;
343        } else {
344            return mInputLocaleStr;
345        }
346    }
347
348    public String[] getEnabledLanguages() {
349        if (mConfigUseSpacebarLanguageSwitcher) {
350            return mLanguageSwitcher.getEnabledLanguages();
351        } else {
352            return mEnabledLanguagesOfCurrentInputMethod.toArray(
353                    new String[mEnabledLanguagesOfCurrentInputMethod.size()]);
354        }
355    }
356
357    public Locale getSystemLocale() {
358        if (mConfigUseSpacebarLanguageSwitcher) {
359            return mLanguageSwitcher.getSystemLocale();
360        } else {
361            return mSystemLocale;
362        }
363    }
364
365    public boolean isSystemLanguageSameAsInputLanguage() {
366        if (mConfigUseSpacebarLanguageSwitcher) {
367            return getSystemLocale().getLanguage().equalsIgnoreCase(
368                    getInputLocaleStr().substring(0, 2));
369        } else {
370            return mIsSystemLanguageSameAsInputLanguage;
371        }
372    }
373
374    public void onConfigurationChanged(Configuration conf) {
375        final Locale systemLocale = conf.locale;
376        // If system configuration was changed, update all parameters.
377        if (!TextUtils.equals(systemLocale.toString(), mSystemLocale.toString())) {
378            if (mConfigUseSpacebarLanguageSwitcher) {
379                // If the system locale changes and is different from the saved
380                // locale (mSystemLocale), then reload the input locale list from the
381                // latin ime settings (shared prefs) and reset the input locale
382                // to the first one.
383                mLanguageSwitcher.loadLocales(mPrefs);
384                mLanguageSwitcher.setSystemLocale(systemLocale);
385            } else {
386                updateAllParameters();
387            }
388        }
389    }
390
391    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
392        if (mConfigUseSpacebarLanguageSwitcher) {
393            if (Settings.PREF_SELECTED_LANGUAGES.equals(key)) {
394                mLanguageSwitcher.loadLocales(sharedPreferences);
395            }
396        }
397    }
398
399    /**
400     * Change system locale for this application
401     * @param newLocale
402     * @return oldLocale
403     */
404    public Locale changeSystemLocale(Locale newLocale) {
405        Configuration conf = mResources.getConfiguration();
406        Locale oldLocale = conf.locale;
407        conf.locale = newLocale;
408        mResources.updateConfiguration(conf, mResources.getDisplayMetrics());
409        return oldLocale;
410    }
411
412    public boolean isKeyboardMode() {
413        return KEYBOARD_MODE.equals(mMode);
414    }
415
416
417    ///////////////////////////
418    // Voice Input functions //
419    ///////////////////////////
420
421    public boolean setVoiceInput(VoiceInput vi) {
422        if (mVoiceInput == null && vi != null) {
423            mVoiceInput = vi;
424            if (isVoiceMode()) {
425                if (DBG) {
426                    Log.d(TAG, "Set and call voice input.");
427                }
428                triggerVoiceIME();
429                return true;
430            }
431        }
432        return false;
433    }
434
435    public boolean isVoiceMode() {
436        return VOICE_MODE.equals(mMode);
437    }
438
439    private void triggerVoiceIME() {
440        if (!mService.isInputViewShown()) return;
441        VoiceIMEConnector.getInstance().startListening(false,
442                KeyboardSwitcher.getInstance().getInputView().getWindowToken());
443    }
444
445    //////////////////////////////////////
446    // Spacebar Language Switch support //
447    //////////////////////////////////////
448
449    private LanguageSwitcher mLanguageSwitcher;
450
451    public static String getFullDisplayName(Locale locale, boolean returnsNameInThisLocale) {
452        if (returnsNameInThisLocale) {
453            return toTitleCase(SubtypeLocale.getFullDisplayName(locale));
454        } else {
455            return toTitleCase(locale.getDisplayName());
456        }
457    }
458
459    public static String getDisplayLanguage(Locale locale) {
460        return toTitleCase(locale.getDisplayLanguage(locale));
461    }
462
463    public static String getShortDisplayLanguage(Locale locale) {
464        return toTitleCase(locale.getLanguage());
465    }
466
467    private static String toTitleCase(String s) {
468        if (s.length() == 0) {
469            return s;
470        }
471        return Character.toUpperCase(s.charAt(0)) + s.substring(1);
472    }
473
474    private void updateForSpacebarLanguageSwitch() {
475        // We need to update mNeedsToDisplayLanguage in onStartInputView because
476        // getEnabledKeyboardLocaleCount could have been changed.
477        mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1
478                && getSystemLocale().getLanguage().equalsIgnoreCase(
479                        getInputLocale().getLanguage()));
480    }
481
482    public String getInputLanguageName() {
483        return getDisplayLanguage(getInputLocale());
484    }
485
486    public String getNextInputLanguageName() {
487        if (mConfigUseSpacebarLanguageSwitcher) {
488            return getDisplayLanguage(mLanguageSwitcher.getNextInputLocale());
489        } else {
490            return "";
491        }
492    }
493
494    public String getPreviousInputLanguageName() {
495        if (mConfigUseSpacebarLanguageSwitcher) {
496            return getDisplayLanguage(mLanguageSwitcher.getPrevInputLocale());
497        } else {
498            return "";
499        }
500    }
501
502    // A list of locales which are supported by default for voice input, unless we get a
503    // different list from Gservices.
504    private static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES =
505            "en " +
506            "en_US " +
507            "en_GB " +
508            "en_AU " +
509            "en_CA " +
510            "en_IE " +
511            "en_IN " +
512            "en_NZ " +
513            "en_SG " +
514            "en_ZA ";
515
516    public boolean isVoiceSupported(String locale) {
517        // Get the current list of supported locales and check the current locale against that
518        // list. We cache this value so as not to check it every time the user starts a voice
519        // input. Because this method is called by onStartInputView, this should mean that as
520        // long as the locale doesn't change while the user is keeping the IME open, the
521        // value should never be stale.
522        String supportedLocalesString = SettingsUtil.getSettingsString(
523                mService.getContentResolver(),
524                SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES,
525                DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES);
526        List<String> voiceInputSupportedLocales = Arrays.asList(
527                supportedLocalesString.split("\\s+"));
528        return voiceInputSupportedLocales.contains(locale);
529    }
530
531    public void loadSettings() {
532        if (mConfigUseSpacebarLanguageSwitcher) {
533            mLanguageSwitcher.loadLocales(mPrefs);
534        }
535    }
536
537    public void toggleLanguage(boolean reset, boolean next) {
538        if (mConfigUseSpacebarLanguageSwitcher) {
539            if (reset) {
540                mLanguageSwitcher.reset();
541            } else {
542                if (next) {
543                    mLanguageSwitcher.next();
544                } else {
545                    mLanguageSwitcher.prev();
546                }
547            }
548            mLanguageSwitcher.persist(mPrefs);
549        }
550    }
551
552    private void initLanguageSwitcher(LatinIME service) {
553        final Configuration conf = service.getResources().getConfiguration();
554        mLanguageSwitcher = new LanguageSwitcher(service);
555        mLanguageSwitcher.loadLocales(mPrefs);
556        mLanguageSwitcher.setSystemLocale(conf.locale);
557    }
558}
559