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