Home | History | Annotate | Download | only in utils
      1 /*
      2  * Copyright (C) 2012 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.KEYBOARD_MODE;
     20 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE;
     21 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
     22 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE;
     23 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
     24 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
     25 
     26 import android.os.Build;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 import android.view.inputmethod.InputMethodSubtype;
     30 
     31 import com.android.inputmethod.annotations.UsedForTesting;
     32 import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
     33 import com.android.inputmethod.latin.R;
     34 
     35 import java.util.ArrayList;
     36 import java.util.Arrays;
     37 
     38 public final class AdditionalSubtypeUtils {
     39     private static final String TAG = AdditionalSubtypeUtils.class.getSimpleName();
     40 
     41     private static final InputMethodSubtype[] EMPTY_SUBTYPE_ARRAY = new InputMethodSubtype[0];
     42 
     43     private AdditionalSubtypeUtils() {
     44         // This utility class is not publicly instantiable.
     45     }
     46 
     47     @UsedForTesting
     48     public static boolean isAdditionalSubtype(final InputMethodSubtype subtype) {
     49         return subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE);
     50     }
     51 
     52     private static final String LOCALE_AND_LAYOUT_SEPARATOR = ":";
     53     private static final int INDEX_OF_LOCALE = 0;
     54     private static final int INDEX_OF_KEYBOARD_LAYOUT = 1;
     55     private static final int INDEX_OF_EXTRA_VALUE = 2;
     56     private static final int LENGTH_WITHOUT_EXTRA_VALUE = (INDEX_OF_KEYBOARD_LAYOUT + 1);
     57     private static final int LENGTH_WITH_EXTRA_VALUE = (INDEX_OF_EXTRA_VALUE + 1);
     58     private static final String PREF_SUBTYPE_SEPARATOR = ";";
     59 
     60     private static InputMethodSubtype createAdditionalSubtypeInternal(
     61             final String localeString, final String keyboardLayoutSetName,
     62             final boolean isAsciiCapable, final boolean isEmojiCapable) {
     63         final int nameId = SubtypeLocaleUtils.getSubtypeNameId(localeString, keyboardLayoutSetName);
     64         final String platformVersionDependentExtraValues = getPlatformVersionDependentExtraValue(
     65                 localeString, keyboardLayoutSetName, isAsciiCapable, isEmojiCapable);
     66         final int platformVersionIndependentSubtypeId =
     67                 getPlatformVersionIndependentSubtypeId(localeString, keyboardLayoutSetName);
     68         // NOTE: In KitKat and later, InputMethodSubtypeBuilder#setIsAsciiCapable is also available.
     69         // TODO: Use InputMethodSubtypeBuilder#setIsAsciiCapable when appropriate.
     70         return InputMethodSubtypeCompatUtils.newInputMethodSubtype(nameId,
     71                 R.drawable.ic_ime_switcher_dark, localeString, KEYBOARD_MODE,
     72                 platformVersionDependentExtraValues,
     73                 false /* isAuxiliary */, false /* overrideImplicitlyEnabledSubtype */,
     74                 platformVersionIndependentSubtypeId);
     75     }
     76 
     77     public static InputMethodSubtype createDummyAdditionalSubtype(
     78             final String localeString, final String keyboardLayoutSetName) {
     79         return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName,
     80                 false /* isAsciiCapable */, false /* isEmojiCapable */);
     81     }
     82 
     83     public static InputMethodSubtype createAsciiEmojiCapableAdditionalSubtype(
     84             final String localeString, final String keyboardLayoutSetName) {
     85         return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName,
     86                 true /* isAsciiCapable */, true /* isEmojiCapable */);
     87     }
     88 
     89     public static String getPrefSubtype(final InputMethodSubtype subtype) {
     90         final String localeString = subtype.getLocale();
     91         final String keyboardLayoutSetName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
     92         final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
     93         final String extraValue = StringUtils.removeFromCommaSplittableTextIfExists(
     94                 layoutExtraValue, StringUtils.removeFromCommaSplittableTextIfExists(
     95                         IS_ADDITIONAL_SUBTYPE, subtype.getExtraValue()));
     96         final String basePrefSubtype = localeString + LOCALE_AND_LAYOUT_SEPARATOR
     97                 + keyboardLayoutSetName;
     98         return extraValue.isEmpty() ? basePrefSubtype
     99                 : basePrefSubtype + LOCALE_AND_LAYOUT_SEPARATOR + extraValue;
    100     }
    101 
    102     public static InputMethodSubtype[] createAdditionalSubtypesArray(final String prefSubtypes) {
    103         if (TextUtils.isEmpty(prefSubtypes)) {
    104             return EMPTY_SUBTYPE_ARRAY;
    105         }
    106         final String[] prefSubtypeArray = prefSubtypes.split(PREF_SUBTYPE_SEPARATOR);
    107         final ArrayList<InputMethodSubtype> subtypesList = new ArrayList<>(prefSubtypeArray.length);
    108         for (final String prefSubtype : prefSubtypeArray) {
    109             final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR);
    110             if (elems.length != LENGTH_WITHOUT_EXTRA_VALUE
    111                     && elems.length != LENGTH_WITH_EXTRA_VALUE) {
    112                 Log.w(TAG, "Unknown additional subtype specified: " + prefSubtype + " in "
    113                         + prefSubtypes);
    114                 continue;
    115             }
    116             final String localeString = elems[INDEX_OF_LOCALE];
    117             final String keyboardLayoutSetName = elems[INDEX_OF_KEYBOARD_LAYOUT];
    118             // Here we assume that all the additional subtypes have AsciiCapable and EmojiCapable.
    119             // This is actually what the setting dialog for additional subtype is doing.
    120             final InputMethodSubtype subtype = createAsciiEmojiCapableAdditionalSubtype(
    121                     localeString, keyboardLayoutSetName);
    122             if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) {
    123                 // Skip unknown keyboard layout subtype. This may happen when predefined keyboard
    124                 // layout has been removed.
    125                 continue;
    126             }
    127             subtypesList.add(subtype);
    128         }
    129         return subtypesList.toArray(new InputMethodSubtype[subtypesList.size()]);
    130     }
    131 
    132     public static String createPrefSubtypes(final InputMethodSubtype[] subtypes) {
    133         if (subtypes == null || subtypes.length == 0) {
    134             return "";
    135         }
    136         final StringBuilder sb = new StringBuilder();
    137         for (final InputMethodSubtype subtype : subtypes) {
    138             if (sb.length() > 0) {
    139                 sb.append(PREF_SUBTYPE_SEPARATOR);
    140             }
    141             sb.append(getPrefSubtype(subtype));
    142         }
    143         return sb.toString();
    144     }
    145 
    146     public static String createPrefSubtypes(final String[] prefSubtypes) {
    147         if (prefSubtypes == null || prefSubtypes.length == 0) {
    148             return "";
    149         }
    150         final StringBuilder sb = new StringBuilder();
    151         for (final String prefSubtype : prefSubtypes) {
    152             if (sb.length() > 0) {
    153                 sb.append(PREF_SUBTYPE_SEPARATOR);
    154             }
    155             sb.append(prefSubtype);
    156         }
    157         return sb.toString();
    158     }
    159 
    160     /**
    161      * Returns the extra value that is optimized for the running OS.
    162      * <p>
    163      * Historically the extra value has been used as the last resort to annotate various kinds of
    164      * attributes. Some of these attributes are valid only on some platform versions. Thus we cannot
    165      * assume that the extra values stored in a persistent storage are always valid. We need to
    166      * regenerate the extra value on the fly instead.
    167      * </p>
    168      * @param localeString the locale string (e.g., "en_US").
    169      * @param keyboardLayoutSetName the keyboard layout set name (e.g., "dvorak").
    170      * @param isAsciiCapable true when ASCII characters are supported with this layout.
    171      * @param isEmojiCapable true when Unicode Emoji characters are supported with this layout.
    172      * @return extra value that is optimized for the running OS.
    173      * @see #getPlatformVersionIndependentSubtypeId(String, String)
    174      */
    175     private static String getPlatformVersionDependentExtraValue(final String localeString,
    176             final String keyboardLayoutSetName, final boolean isAsciiCapable,
    177             final boolean isEmojiCapable) {
    178         final ArrayList<String> extraValueItems = new ArrayList<>();
    179         extraValueItems.add(KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName);
    180         if (isAsciiCapable) {
    181             extraValueItems.add(ASCII_CAPABLE);
    182         }
    183         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
    184                 SubtypeLocaleUtils.isExceptionalLocale(localeString)) {
    185             extraValueItems.add(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" +
    186                     SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(keyboardLayoutSetName));
    187         }
    188         if (isEmojiCapable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    189             extraValueItems.add(EMOJI_CAPABLE);
    190         }
    191         extraValueItems.add(IS_ADDITIONAL_SUBTYPE);
    192         return TextUtils.join(",", extraValueItems);
    193     }
    194 
    195     /**
    196      * Returns the subtype ID that is supposed to be compatible between different version of OSes.
    197      * <p>
    198      * From the compatibility point of view, it is important to keep subtype id predictable and
    199      * stable between different OSes. For this purpose, the calculation code in this method is
    200      * carefully chosen and then fixed. Treat the following code as no more or less than a
    201      * hash function. Each component to be hashed can be different from the corresponding value
    202      * that is used to instantiate {@link InputMethodSubtype} actually.
    203      * For example, you don't need to update <code>compatibilityExtraValueItems</code> in this
    204      * method even when we need to add some new extra values for the actual instance of
    205      * {@link InputMethodSubtype}.
    206      * </p>
    207      * @param localeString the locale string (e.g., "en_US").
    208      * @param keyboardLayoutSetName the keyboard layout set name (e.g., "dvorak").
    209      * @return a platform-version independent subtype ID.
    210      * @see #getPlatformVersionDependentExtraValue(String, String, boolean, boolean)
    211      */
    212     private static int getPlatformVersionIndependentSubtypeId(final String localeString,
    213             final String keyboardLayoutSetName) {
    214         // For compatibility reasons, we concatenate the extra values in the following order.
    215         // - KeyboardLayoutSet
    216         // - AsciiCapable
    217         // - UntranslatableReplacementStringInSubtypeName
    218         // - EmojiCapable
    219         // - isAdditionalSubtype
    220         final ArrayList<String> compatibilityExtraValueItems = new ArrayList<>();
    221         compatibilityExtraValueItems.add(KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName);
    222         compatibilityExtraValueItems.add(ASCII_CAPABLE);
    223         if (SubtypeLocaleUtils.isExceptionalLocale(localeString)) {
    224             compatibilityExtraValueItems.add(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" +
    225                     SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(keyboardLayoutSetName));
    226         }
    227         compatibilityExtraValueItems.add(EMOJI_CAPABLE);
    228         compatibilityExtraValueItems.add(IS_ADDITIONAL_SUBTYPE);
    229         final String compatibilityExtraValues = TextUtils.join(",", compatibilityExtraValueItems);
    230         return Arrays.hashCode(new Object[] {
    231                 localeString,
    232                 KEYBOARD_MODE,
    233                 compatibilityExtraValues,
    234                 false /* isAuxiliary */,
    235                 false /* overrideImplicitlyEnabledSubtype */ });
    236     }
    237 }
    238