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