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 AdditionalSubtypeUtils()43 private AdditionalSubtypeUtils() { 44 // This utility class is not publicly instantiable. 45 } 46 47 @UsedForTesting isAdditionalSubtype(final InputMethodSubtype subtype)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 createAdditionalSubtypeInternal( final String localeString, final String keyboardLayoutSetName, final boolean isAsciiCapable, final boolean isEmojiCapable)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 createDummyAdditionalSubtype( final String localeString, final String keyboardLayoutSetName)77 public static InputMethodSubtype createDummyAdditionalSubtype( 78 final String localeString, final String keyboardLayoutSetName) { 79 return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName, 80 false /* isAsciiCapable */, false /* isEmojiCapable */); 81 } 82 createAsciiEmojiCapableAdditionalSubtype( final String localeString, final String keyboardLayoutSetName)83 public static InputMethodSubtype createAsciiEmojiCapableAdditionalSubtype( 84 final String localeString, final String keyboardLayoutSetName) { 85 return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName, 86 true /* isAsciiCapable */, true /* isEmojiCapable */); 87 } 88 getPrefSubtype(final InputMethodSubtype subtype)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 createAdditionalSubtypesArray(final String prefSubtypes)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 createPrefSubtypes(final InputMethodSubtype[] subtypes)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 createPrefSubtypes(final String[] prefSubtypes)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 */ getPlatformVersionDependentExtraValue(final String localeString, final String keyboardLayoutSetName, final boolean isAsciiCapable, final boolean isEmojiCapable)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 */ getPlatformVersionIndependentSubtypeId(final String localeString, final String keyboardLayoutSetName)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