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