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