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