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.common.Constants.Subtype.KEYBOARD_MODE;
20import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.ASCII_CAPABLE;
21import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
22import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE;
23import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
24import static com.android.inputmethod.latin.common.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;
34import com.android.inputmethod.latin.common.StringUtils;
35
36import java.util.ArrayList;
37import java.util.Arrays;
38
39public 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