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.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 
     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  Espaol (EE.UU.)        exception
    247     //  fr    azerty  F  Franais
    248     //  fr_CA qwerty  F  Franais (Canada)
    249     //  fr_CH swiss   F  Franais (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  Franais (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