1/*
2 * Copyright (C) 2011 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 static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
20import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
21
22import android.content.Context;
23import android.content.res.Resources;
24import android.os.Build;
25import android.util.Log;
26import android.view.inputmethod.InputMethodSubtype;
27
28import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
29
30import java.util.HashMap;
31import java.util.Locale;
32
33public final class SubtypeLocale {
34    static final String TAG = SubtypeLocale.class.getSimpleName();
35    // This class must be located in the same package as LatinIME.java.
36    private static final String RESOURCE_PACKAGE_NAME =
37            DictionaryFactory.class.getPackage().getName();
38
39    // Special language code to represent "no language".
40    public static final String NO_LANGUAGE = "zz";
41    public static final String QWERTY = "qwerty";
42    public static final int UNKNOWN_KEYBOARD_LAYOUT = R.string.subtype_generic;
43
44    private static boolean sInitialized = false;
45    private static Resources sResources;
46    private static String[] sPredefinedKeyboardLayoutSet;
47    // Keyboard layout to its display name map.
48    private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap =
49            CollectionUtils.newHashMap();
50    // Keyboard layout to subtype name resource id map.
51    private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap =
52            CollectionUtils.newHashMap();
53    // Exceptional locale to subtype name resource id map.
54    private static final HashMap<String, Integer> sExceptionalLocaleToNameIdsMap =
55            CollectionUtils.newHashMap();
56    // Exceptional locale to subtype name with layout resource id map.
57    private static final HashMap<String, Integer> sExceptionalLocaleToWithLayoutNameIdsMap =
58            CollectionUtils.newHashMap();
59    private static final String SUBTYPE_NAME_RESOURCE_PREFIX =
60            "string/subtype_";
61    private static final String SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX =
62            "string/subtype_generic_";
63    private static final String SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX =
64            "string/subtype_with_layout_";
65    private static final String SUBTYPE_NAME_RESOURCE_NO_LANGUAGE_PREFIX =
66            "string/subtype_no_language_";
67    // Keyboard layout set name for the subtypes that don't have a keyboardLayoutSet extra value.
68    // This is for compatibility to keep the same subtype ids as pre-JellyBean.
69    private static final HashMap<String, String> sLocaleAndExtraValueToKeyboardLayoutSetMap =
70            CollectionUtils.newHashMap();
71
72    private SubtypeLocale() {
73        // Intentional empty constructor for utility class.
74    }
75
76    // Note that this initialization method can be called multiple times.
77    public static synchronized void init(final Context context) {
78        if (sInitialized) return;
79
80        final Resources res = context.getResources();
81        sResources = res;
82
83        final String[] predefinedLayoutSet = res.getStringArray(R.array.predefined_layouts);
84        sPredefinedKeyboardLayoutSet = predefinedLayoutSet;
85        final String[] layoutDisplayNames = res.getStringArray(
86                R.array.predefined_layout_display_names);
87        for (int i = 0; i < predefinedLayoutSet.length; i++) {
88            final String layoutName = predefinedLayoutSet[i];
89            sKeyboardLayoutToDisplayNameMap.put(layoutName, layoutDisplayNames[i]);
90            final String resourceName = SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX + layoutName;
91            final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME);
92            sKeyboardLayoutToNameIdsMap.put(layoutName, resId);
93            // Register subtype name resource id of "No language" with key "zz_<layout>"
94            final String noLanguageResName = SUBTYPE_NAME_RESOURCE_NO_LANGUAGE_PREFIX + layoutName;
95            final int noLanguageResId = res.getIdentifier(
96                    noLanguageResName, null, RESOURCE_PACKAGE_NAME);
97            final String key = getNoLanguageLayoutKey(layoutName);
98            sKeyboardLayoutToNameIdsMap.put(key, noLanguageResId);
99        }
100
101        final String[] exceptionalLocales = res.getStringArray(
102                R.array.subtype_locale_exception_keys);
103        for (int i = 0; i < exceptionalLocales.length; i++) {
104            final String localeString = exceptionalLocales[i];
105            final String resourceName = SUBTYPE_NAME_RESOURCE_PREFIX + localeString;
106            final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME);
107            sExceptionalLocaleToNameIdsMap.put(localeString, resId);
108            final String resourceNameWithLayout =
109                    SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX + localeString;
110            final int resIdWithLayout = res.getIdentifier(
111                    resourceNameWithLayout, null, RESOURCE_PACKAGE_NAME);
112            sExceptionalLocaleToWithLayoutNameIdsMap.put(localeString, resIdWithLayout);
113        }
114
115        final String[] keyboardLayoutSetMap = res.getStringArray(
116                R.array.locale_and_extra_value_to_keyboard_layout_set_map);
117        for (int i = 0; i + 1 < keyboardLayoutSetMap.length; i += 2) {
118            final String key = keyboardLayoutSetMap[i];
119            final String keyboardLayoutSet = keyboardLayoutSetMap[i + 1];
120            sLocaleAndExtraValueToKeyboardLayoutSetMap.put(key, keyboardLayoutSet);
121        }
122
123        sInitialized = true;
124    }
125
126    public static String[] getPredefinedKeyboardLayoutSet() {
127        return sPredefinedKeyboardLayoutSet;
128    }
129
130    public static boolean isExceptionalLocale(final String localeString) {
131        return sExceptionalLocaleToNameIdsMap.containsKey(localeString);
132    }
133
134    private static final String getNoLanguageLayoutKey(final String keyboardLayoutName) {
135        return NO_LANGUAGE + "_" + keyboardLayoutName;
136    }
137
138    public static int getSubtypeNameId(final String localeString, final String keyboardLayoutName) {
139        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
140                && isExceptionalLocale(localeString)) {
141            return sExceptionalLocaleToWithLayoutNameIdsMap.get(localeString);
142        }
143        final String key = NO_LANGUAGE.equals(localeString)
144                ? getNoLanguageLayoutKey(keyboardLayoutName)
145                : keyboardLayoutName;
146        final Integer nameId = sKeyboardLayoutToNameIdsMap.get(key);
147        return nameId == null ? UNKNOWN_KEYBOARD_LAYOUT : nameId;
148    }
149
150    private static Locale getDisplayLocaleOfSubtypeLocale(final String localeString) {
151        if (NO_LANGUAGE.equals(localeString)) {
152            return sResources.getConfiguration().locale;
153        }
154        return LocaleUtils.constructLocaleFromString(localeString);
155    }
156
157    public static String getSubtypeLocaleDisplayNameInSystemLocale(final String localeString) {
158        final Locale displayLocale = sResources.getConfiguration().locale;
159        return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale);
160    }
161
162    public static String getSubtypeLocaleDisplayName(final String localeString) {
163        final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString);
164        return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale);
165    }
166
167    private static String getSubtypeLocaleDisplayNameInternal(final String localeString,
168            final Locale displayLocale) {
169        final Integer exceptionalNameResId = sExceptionalLocaleToNameIdsMap.get(localeString);
170        final String displayName;
171        if (exceptionalNameResId != null) {
172            final RunInLocale<String> getExceptionalName = new RunInLocale<String>() {
173                @Override
174                protected String job(final Resources res) {
175                    return res.getString(exceptionalNameResId);
176                }
177            };
178            displayName = getExceptionalName.runInLocale(sResources, displayLocale);
179        } else if (NO_LANGUAGE.equals(localeString)) {
180            // No language subtype should be displayed in system locale.
181            return sResources.getString(R.string.subtype_no_language);
182        } else {
183            final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
184            displayName = locale.getDisplayName(displayLocale);
185        }
186        return StringUtils.capitalizeFirstCodePoint(displayName, displayLocale);
187    }
188
189    // InputMethodSubtype's display name in its locale.
190    //        isAdditionalSubtype (T=true, F=false)
191    // locale layout  |  display name
192    // ------ ------- - ----------------------
193    //  en_US qwerty  F  English (US)            exception
194    //  en_GB qwerty  F  English (UK)            exception
195    //  es_US spanish F  Español (EE.UU.)        exception
196    //  fr    azerty  F  Français
197    //  fr_CA qwerty  F  Français (Canada)
198    //  de    qwertz  F  Deutsch
199    //  zz    qwerty  F  No language (QWERTY)    in system locale
200    //  fr    qwertz  T  Français (QWERTZ)
201    //  de    qwerty  T  Deutsch (QWERTY)
202    //  en_US azerty  T  English (US) (AZERTY)   exception
203    //  zz    azerty  T  No language (AZERTY)    in system locale
204
205    private static String getReplacementString(final InputMethodSubtype subtype,
206            final Locale displayLocale) {
207        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
208                && subtype.containsExtraValueKey(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)) {
209            return subtype.getExtraValueOf(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME);
210        } else {
211            return getSubtypeLocaleDisplayNameInternal(subtype.getLocale(), displayLocale);
212        }
213    }
214
215    public static String getSubtypeDisplayNameInSystemLocale(final InputMethodSubtype subtype) {
216        final Locale displayLocale = sResources.getConfiguration().locale;
217        return getSubtypeDisplayNameInternal(subtype, displayLocale);
218    }
219
220    public static String getSubtypeDisplayName(final InputMethodSubtype subtype) {
221        final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(subtype.getLocale());
222        return getSubtypeDisplayNameInternal(subtype, displayLocale);
223    }
224
225    private static String getSubtypeDisplayNameInternal(final InputMethodSubtype subtype,
226            final Locale displayLocale) {
227        final String replacementString = getReplacementString(subtype, displayLocale);
228        final int nameResId = subtype.getNameResId();
229        final RunInLocale<String> getSubtypeName = new RunInLocale<String>() {
230            @Override
231            protected String job(final Resources res) {
232                try {
233                    return res.getString(nameResId, replacementString);
234                } catch (Resources.NotFoundException e) {
235                    // TODO: Remove this catch when InputMethodManager.getCurrentInputMethodSubtype
236                    // is fixed.
237                    Log.w(TAG, "Unknown subtype: mode=" + subtype.getMode()
238                            + " nameResId=" + subtype.getNameResId()
239                            + " locale=" + subtype.getLocale()
240                            + " extra=" + subtype.getExtraValue()
241                            + "\n" + Utils.getStackTrace());
242                    return "";
243                }
244            }
245        };
246        return StringUtils.capitalizeFirstCodePoint(
247                getSubtypeName.runInLocale(sResources, displayLocale), displayLocale);
248    }
249
250    public static boolean isNoLanguage(final InputMethodSubtype subtype) {
251        final String localeString = subtype.getLocale();
252        return NO_LANGUAGE.equals(localeString);
253    }
254
255    public static Locale getSubtypeLocale(final InputMethodSubtype subtype) {
256        final String localeString = subtype.getLocale();
257        return LocaleUtils.constructLocaleFromString(localeString);
258    }
259
260    public static String getKeyboardLayoutSetDisplayName(final InputMethodSubtype subtype) {
261        final String layoutName = getKeyboardLayoutSetName(subtype);
262        return getKeyboardLayoutSetDisplayName(layoutName);
263    }
264
265    public static String getKeyboardLayoutSetDisplayName(final String layoutName) {
266        return sKeyboardLayoutToDisplayNameMap.get(layoutName);
267    }
268
269    public static String getKeyboardLayoutSetName(final InputMethodSubtype subtype) {
270        String keyboardLayoutSet = subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET);
271        if (keyboardLayoutSet == null) {
272            // This subtype doesn't have a keyboardLayoutSet extra value, so lookup its keyboard
273            // layout set in sLocaleAndExtraValueToKeyboardLayoutSetMap to keep it compatible with
274            // pre-JellyBean.
275            final String key = subtype.getLocale() + ":" + subtype.getExtraValue();
276            keyboardLayoutSet = sLocaleAndExtraValueToKeyboardLayoutSetMap.get(key);
277        }
278        // TODO: Remove this null check when InputMethodManager.getCurrentInputMethodSubtype is
279        // fixed.
280        if (keyboardLayoutSet == null) {
281            android.util.Log.w(TAG, "KeyboardLayoutSet not found, use QWERTY: " +
282                    "locale=" + subtype.getLocale() + " extraValue=" + subtype.getExtraValue());
283            return QWERTY;
284        }
285        return keyboardLayoutSet;
286    }
287}
288