Home | History | Annotate | Download | only in utils
      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 
     73     private SubtypeLocaleUtils() {
     74         // Intentional empty constructor for utility class.
     75     }
     76 
     77     // Note that this initialization method can be called multiple times.
     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 
     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 
    132     public static String[] getPredefinedKeyboardLayoutSet() {
    133         return sPredefinedKeyboardLayoutSet;
    134     }
    135 
    136     public static boolean isExceptionalLocale(final String localeString) {
    137         return sExceptionalLocaleToNameIdsMap.containsKey(localeString);
    138     }
    139 
    140     private static final String getNoLanguageLayoutKey(final String keyboardLayoutName) {
    141         return NO_LANGUAGE + "_" + keyboardLayoutName;
    142     }
    143 
    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 
    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 
    163     public static String getSubtypeLocaleDisplayNameInSystemLocale(final String localeString) {
    164         final Locale displayLocale = sResources.getConfiguration().locale;
    165         return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale);
    166     }
    167 
    168     public static String getSubtypeLocaleDisplayName(final String localeString) {
    169         final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString);
    170         return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale);
    171     }
    172 
    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 
    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  Espaol (EE.UU.)        exception
    209     //  fr    azerty  F  Franais
    210     //  fr_CA qwerty  F  Franais (Canada)
    211     //  fr_CH swiss   F  Franais (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  Franais (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 
    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 
    230     public static String getSubtypeDisplayNameInSystemLocale(final InputMethodSubtype subtype) {
    231         final Locale displayLocale = sResources.getConfiguration().locale;
    232         return getSubtypeDisplayNameInternal(subtype, displayLocale);
    233     }
    234 
    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 
    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 
    267     public static boolean isNoLanguage(final InputMethodSubtype subtype) {
    268         final String localeString = subtype.getLocale();
    269         return NO_LANGUAGE.equals(localeString);
    270     }
    271 
    272     public static Locale getSubtypeLocale(final InputMethodSubtype subtype) {
    273         final String localeString = subtype.getLocale();
    274         return LocaleUtils.constructLocaleFromString(localeString);
    275     }
    276 
    277     public static String getKeyboardLayoutSetDisplayName(final InputMethodSubtype subtype) {
    278         final String layoutName = getKeyboardLayoutSetName(subtype);
    279         return getKeyboardLayoutSetDisplayName(layoutName);
    280     }
    281 
    282     public static String getKeyboardLayoutSetDisplayName(final String layoutName) {
    283         return sKeyboardLayoutToDisplayNameMap.get(layoutName);
    284     }
    285 
    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 
    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 
    321     public static boolean isRtlLanguage(final InputMethodSubtype subtype) {
    322         return isRtlLanguage(getSubtypeLocale(subtype));
    323     }
    324 
    325     public static String getCombiningRulesExtraValue(final InputMethodSubtype subtype) {
    326         return subtype.getExtraValueOf(Constants.Subtype.ExtraValue.COMBINING_RULES);
    327     }
    328 }
    329