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