AdditionalSubtypeUtils.java revision 3895d7f8dc2e4999947f61220b86fa148f433413
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
17package com.android.inputmethod.latin.utils;
18
19import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE;
20import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE;
21import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
22import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE;
23import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
24import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
25
26import android.os.Build;
27import android.text.TextUtils;
28import android.util.Log;
29import android.view.inputmethod.InputMethodSubtype;
30
31import com.android.inputmethod.annotations.UsedForTesting;
32import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
33import com.android.inputmethod.latin.R;
34
35import java.util.ArrayList;
36import java.util.Arrays;
37
38public 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