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