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.common.Constants.Subtype.ExtraValue.COMBINING_RULES;
20 import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
21 import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
22 
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.os.Build;
26 import android.util.Log;
27 import android.view.inputmethod.InputMethodSubtype;
28 
29 import com.android.inputmethod.latin.R;
30 import com.android.inputmethod.latin.common.LocaleUtils;
31 import com.android.inputmethod.latin.common.StringUtils;
32 
33 import java.util.HashMap;
34 import java.util.Locale;
35 
36 import javax.annotation.Nonnull;
37 import javax.annotation.Nullable;
38 
39 /**
40  * A helper class to deal with subtype locales.
41   */
42 // TODO: consolidate this into RichInputMethodSubtype
43 public 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 
SubtypeLocaleUtils()85     private SubtypeLocaleUtils() {
86         // Intentional empty constructor for utility class.
87     }
88 
89     // Note that this initialization method can be called multiple times.
init(final Context context)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 
initLocked(final Context context)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 
isExceptionalLocale(final String localeString)152     public static boolean isExceptionalLocale(final String localeString) {
153         return sExceptionalLocaleToNameIdsMap.containsKey(localeString);
154     }
155 
getNoLanguageLayoutKey(final String keyboardLayoutName)156     private static final String getNoLanguageLayoutKey(final String keyboardLayoutName) {
157         return NO_LANGUAGE + "_" + keyboardLayoutName;
158     }
159 
getSubtypeNameId(final String localeString, final String keyboardLayoutName)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
getDisplayLocaleOfSubtypeLocale(@onnull final String localeString)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 
getSubtypeLocaleDisplayNameInSystemLocale( @onnull final String localeString)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
getSubtypeLocaleDisplayName(@onnull final String localeString)190     public static String getSubtypeLocaleDisplayName(@Nonnull final String localeString) {
191         final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString);
192         return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale);
193     }
194 
195     @Nonnull
getSubtypeLanguageDisplayName(@onnull final String localeString)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
getSubtypeLocaleDisplayNameInternal(@onnull final String localeString, @Nonnull final Locale displayLocale)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
getReplacementString(@onnull final InputMethodSubtype subtype, @Nonnull final Locale displayLocale)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
getSubtypeDisplayNameInSystemLocale( @onnull final InputMethodSubtype subtype)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
getSubtypeNameForLogging(@ullable final InputMethodSubtype subtype)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
getSubtypeDisplayNameInternal(@onnull final InputMethodSubtype subtype, @Nonnull final Locale displayLocale)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
getSubtypeLocale(@onnull final InputMethodSubtype subtype)311     public static Locale getSubtypeLocale(@Nonnull final InputMethodSubtype subtype) {
312         final String localeString = subtype.getLocale();
313         return LocaleUtils.constructLocaleFromString(localeString);
314     }
315 
316     @Nonnull
getKeyboardLayoutSetDisplayName( @onnull final InputMethodSubtype subtype)317     public static String getKeyboardLayoutSetDisplayName(
318             @Nonnull final InputMethodSubtype subtype) {
319         final String layoutName = getKeyboardLayoutSetName(subtype);
320         return getKeyboardLayoutSetDisplayName(layoutName);
321     }
322 
323     @Nonnull
getKeyboardLayoutSetDisplayName(@onnull final String layoutName)324     public static String getKeyboardLayoutSetDisplayName(@Nonnull final String layoutName) {
325         return sKeyboardLayoutToDisplayNameMap.get(layoutName);
326     }
327 
328     @Nonnull
getKeyboardLayoutSetName(final InputMethodSubtype subtype)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 
getCombiningRulesExtraValue(final InputMethodSubtype subtype)348     public static String getCombiningRulesExtraValue(final InputMethodSubtype subtype) {
349         return subtype.getExtraValueOf(COMBINING_RULES);
350     }
351 }
352