InputMethodSubtype.java revision 83e675f5ecf9f5615f3179ac102176faa3ae2596
1/*
2 * Copyright (C) 2010 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 android.view.inputmethod;
18
19import android.content.Context;
20import android.content.pm.ApplicationInfo;
21import android.os.Parcel;
22import android.os.Parcelable;
23import android.text.TextUtils;
24import android.util.Slog;
25
26import java.util.ArrayList;
27import java.util.Arrays;
28import java.util.HashMap;
29import java.util.HashSet;
30import java.util.IllegalFormatException;
31import java.util.List;
32import java.util.Locale;
33
34/**
35 * This class is used to specify meta information of a subtype contained in an input method editor
36 * (IME). Subtype can describe locale (e.g. en_US, fr_FR...) and mode (e.g. voice, keyboard...),
37 * and is used for IME switch and settings. The input method subtype allows the system to bring up
38 * the specified subtype of the designated IME directly.
39 *
40 * <p>It should be defined in an XML resource file of the input method with the
41 * <code>&lt;subtype&gt;</code> element. For more information, see the guide to
42 * <a href="{@docRoot}resources/articles/creating-input-method.html">
43 * Creating an Input Method</a>.</p>
44 */
45public final class InputMethodSubtype implements Parcelable {
46    private static final String TAG = InputMethodSubtype.class.getSimpleName();
47    private static final String EXTRA_VALUE_PAIR_SEPARATOR = ",";
48    private static final String EXTRA_VALUE_KEY_VALUE_SEPARATOR = "=";
49    // TODO: remove this
50    private static final String EXTRA_KEY_UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME =
51            "UntranslatableReplacementStringInSubtypeName";
52
53    private final boolean mIsAuxiliary;
54    private final boolean mOverridesImplicitlyEnabledSubtype;
55    private final int mSubtypeHashCode;
56    private final int mSubtypeIconResId;
57    private final int mSubtypeNameResId;
58    private final String mSubtypeLocale;
59    private final String mSubtypeMode;
60    private final String mSubtypeExtraValue;
61    private HashMap<String, String> mExtraValueHashMapCache;
62
63    /**
64     * Constructor.
65     * @param nameId Resource ID of the subtype name string. The string resource may have exactly
66     * one %s in it. If there is, the %s part will be replaced with the locale's display name by
67     * the formatter. Please refer to {@link #getDisplayName} for details.
68     * @param iconId Resource ID of the subtype icon drawable.
69     * @param locale The locale supported by the subtype
70     * @param mode The mode supported by the subtype
71     * @param extraValue The extra value of the subtype. This string is free-form, but the API
72     * supplies tools to deal with a key-value comma-separated list; see
73     * {@link #containsExtraValueKey} and {@link #getExtraValueOf}.
74     * @param isAuxiliary true when this subtype is auxiliary, false otherwise. An auxiliary
75     * subtype will not be shown in the list of enabled IMEs for choosing the current IME in
76     * the Settings even when this subtype is enabled. Please note that this subtype will still
77     * be shown in the list of IMEs in the IME switcher to allow the user to tentatively switch
78     * to this subtype while an IME is shown. The framework will never switch the current IME to
79     * this subtype by {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}.
80     * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as
81     * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input).
82     * @hide
83     */
84    public InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue,
85            boolean isAuxiliary) {
86        this(nameId, iconId, locale, mode, extraValue, isAuxiliary, false);
87    }
88
89    /**
90     * Constructor.
91     * @param nameId Resource ID of the subtype name string. The string resource may have exactly
92     * one %s in it. If there is, the %s part will be replaced with the locale's display name by
93     * the formatter. Please refer to {@link #getDisplayName} for details.
94     * @param iconId Resource ID of the subtype icon drawable.
95     * @param locale The locale supported by the subtype
96     * @param mode The mode supported by the subtype
97     * @param extraValue The extra value of the subtype. This string is free-form, but the API
98     * supplies tools to deal with a key-value comma-separated list; see
99     * {@link #containsExtraValueKey} and {@link #getExtraValueOf}.
100     * @param isAuxiliary true when this subtype is auxiliary, false otherwise. An auxiliary
101     * subtype will not be shown in the list of enabled IMEs for choosing the current IME in
102     * the Settings even when this subtype is enabled. Please note that this subtype will still
103     * be shown in the list of IMEs in the IME switcher to allow the user to tentatively switch
104     * to this subtype while an IME is shown. The framework will never switch the current IME to
105     * this subtype by {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}.
106     * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as
107     * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input).
108     * @param overridesImplicitlyEnabledSubtype true when this subtype should be enabled by default
109     * if no other subtypes in the IME are enabled explicitly. Note that a subtype with this
110     * parameter being true will not be shown in the list of subtypes in each IME's subtype enabler.
111     * Having an "automatic" subtype is an example use of this flag.
112     */
113    public InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue,
114            boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype) {
115        mSubtypeNameResId = nameId;
116        mSubtypeIconResId = iconId;
117        mSubtypeLocale = locale != null ? locale : "";
118        mSubtypeMode = mode != null ? mode : "";
119        mSubtypeExtraValue = extraValue != null ? extraValue : "";
120        mIsAuxiliary = isAuxiliary;
121        mOverridesImplicitlyEnabledSubtype = overridesImplicitlyEnabledSubtype;
122        mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue,
123                mIsAuxiliary, mOverridesImplicitlyEnabledSubtype);
124    }
125
126    InputMethodSubtype(Parcel source) {
127        String s;
128        mSubtypeNameResId = source.readInt();
129        mSubtypeIconResId = source.readInt();
130        s = source.readString();
131        mSubtypeLocale = s != null ? s : "";
132        s = source.readString();
133        mSubtypeMode = s != null ? s : "";
134        s = source.readString();
135        mSubtypeExtraValue = s != null ? s : "";
136        mIsAuxiliary = (source.readInt() == 1);
137        mOverridesImplicitlyEnabledSubtype = (source.readInt() == 1);
138        mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue,
139                mIsAuxiliary, mOverridesImplicitlyEnabledSubtype);
140    }
141
142    /**
143     * @return Resource ID of the subtype name string.
144     */
145    public int getNameResId() {
146        return mSubtypeNameResId;
147    }
148
149    /**
150     * @return Resource ID of the subtype icon drawable.
151     */
152    public int getIconResId() {
153        return mSubtypeIconResId;
154    }
155
156    /**
157     * @return The locale of the subtype. This method returns the "locale" string parameter passed
158     * to the constructor.
159     */
160    public String getLocale() {
161        return mSubtypeLocale;
162    }
163
164    /**
165     * @return The mode of the subtype.
166     */
167    public String getMode() {
168        return mSubtypeMode;
169    }
170
171    /**
172     * @return The extra value of the subtype.
173     */
174    public String getExtraValue() {
175        return mSubtypeExtraValue;
176    }
177
178    /**
179     * @return true if this subtype is auxiliary, false otherwise. An auxiliary subtype will not be
180     * shown in the list of enabled IMEs for choosing the current IME in the Settings even when this
181     * subtype is enabled. Please note that this subtype will still be shown in the list of IMEs in
182     * the IME switcher to allow the user to tentatively switch to this subtype while an IME is
183     * shown. The framework will never switch the current IME to this subtype by
184     * {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}.
185     * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as
186     * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input).
187     */
188    public boolean isAuxiliary() {
189        return mIsAuxiliary;
190    }
191
192    /**
193     * @return true when this subtype will be enabled by default if no other subtypes in the IME
194     * are enabled explicitly, false otherwise. Note that a subtype with this method returning true
195     * will not be shown in the list of subtypes in each IME's subtype enabler. Having an
196     * "automatic" subtype is an example use of this flag.
197     */
198    public boolean overridesImplicitlyEnabledSubtype() {
199        return mOverridesImplicitlyEnabledSubtype;
200    }
201
202    /**
203     * @param context Context will be used for getting Locale and PackageManager.
204     * @param packageName The package name of the IME
205     * @param appInfo The application info of the IME
206     * @return a display name for this subtype. The string resource of the label (mSubtypeNameResId)
207     * may have exactly one %s in it. If there is, the %s part will be replaced with the locale's
208     * display name by the formatter. If there is not, this method returns the string specified by
209     * mSubtypeNameResId. If mSubtypeNameResId is not specified (== 0), it's up to the framework to
210     * generate an appropriate display name.
211     */
212    public CharSequence getDisplayName(
213            Context context, String packageName, ApplicationInfo appInfo) {
214        final Locale locale = constructLocaleFromString(mSubtypeLocale);
215        final String localeStr = locale != null ? locale.getDisplayName() : mSubtypeLocale;
216        if (mSubtypeNameResId == 0) {
217            return localeStr;
218        }
219        final CharSequence subtypeName = context.getPackageManager().getText(
220                packageName, mSubtypeNameResId, appInfo);
221        if (!TextUtils.isEmpty(subtypeName)) {
222            final String replacementString =
223                    containsExtraValueKey(EXTRA_KEY_UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)
224                            ? getExtraValueOf(EXTRA_KEY_UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)
225                            : localeStr;
226            try {
227                return String.format(
228                        subtypeName.toString(), replacementString != null ? replacementString : "");
229            } catch (IllegalFormatException e) {
230                Slog.w(TAG, "Found illegal format in subtype name("+ subtypeName + "): " + e);
231                return "";
232            }
233        } else {
234            return localeStr;
235        }
236    }
237
238    private HashMap<String, String> getExtraValueHashMap() {
239        if (mExtraValueHashMapCache == null) {
240            mExtraValueHashMapCache = new HashMap<String, String>();
241            final String[] pairs = mSubtypeExtraValue.split(EXTRA_VALUE_PAIR_SEPARATOR);
242            final int N = pairs.length;
243            for (int i = 0; i < N; ++i) {
244                final String[] pair = pairs[i].split(EXTRA_VALUE_KEY_VALUE_SEPARATOR);
245                if (pair.length == 1) {
246                    mExtraValueHashMapCache.put(pair[0], null);
247                } else if (pair.length > 1) {
248                    if (pair.length > 2) {
249                        Slog.w(TAG, "ExtraValue has two or more '='s");
250                    }
251                    mExtraValueHashMapCache.put(pair[0], pair[1]);
252                }
253            }
254        }
255        return mExtraValueHashMapCache;
256    }
257
258    /**
259     * The string of ExtraValue in subtype should be defined as follows:
260     * example: key0,key1=value1,key2,key3,key4=value4
261     * @param key The key of extra value
262     * @return The subtype contains specified the extra value
263     */
264    public boolean containsExtraValueKey(String key) {
265        return getExtraValueHashMap().containsKey(key);
266    }
267
268    /**
269     * The string of ExtraValue in subtype should be defined as follows:
270     * example: key0,key1=value1,key2,key3,key4=value4
271     * @param key The key of extra value
272     * @return The value of the specified key
273     */
274    public String getExtraValueOf(String key) {
275        return getExtraValueHashMap().get(key);
276    }
277
278    @Override
279    public int hashCode() {
280        return mSubtypeHashCode;
281    }
282
283    @Override
284    public boolean equals(Object o) {
285        if (o instanceof InputMethodSubtype) {
286            InputMethodSubtype subtype = (InputMethodSubtype) o;
287            return (subtype.hashCode() == hashCode())
288                && (subtype.getNameResId() == getNameResId())
289                && (subtype.getMode().equals(getMode()))
290                && (subtype.getIconResId() == getIconResId())
291                && (subtype.getLocale().equals(getLocale()))
292                && (subtype.getExtraValue().equals(getExtraValue()))
293                && (subtype.isAuxiliary() == isAuxiliary());
294        }
295        return false;
296    }
297
298    @Override
299    public int describeContents() {
300        return 0;
301    }
302
303    @Override
304    public void writeToParcel(Parcel dest, int parcelableFlags) {
305        dest.writeInt(mSubtypeNameResId);
306        dest.writeInt(mSubtypeIconResId);
307        dest.writeString(mSubtypeLocale);
308        dest.writeString(mSubtypeMode);
309        dest.writeString(mSubtypeExtraValue);
310        dest.writeInt(mIsAuxiliary ? 1 : 0);
311        dest.writeInt(mOverridesImplicitlyEnabledSubtype ? 1 : 0);
312    }
313
314    public static final Parcelable.Creator<InputMethodSubtype> CREATOR
315            = new Parcelable.Creator<InputMethodSubtype>() {
316        @Override
317        public InputMethodSubtype createFromParcel(Parcel source) {
318            return new InputMethodSubtype(source);
319        }
320
321        @Override
322        public InputMethodSubtype[] newArray(int size) {
323            return new InputMethodSubtype[size];
324        }
325    };
326
327    private static Locale constructLocaleFromString(String localeStr) {
328        if (TextUtils.isEmpty(localeStr))
329            return null;
330        String[] localeParams = localeStr.split("_", 3);
331        // The length of localeStr is guaranteed to always return a 1 <= value <= 3
332        // because localeStr is not empty.
333        if (localeParams.length == 1) {
334            return new Locale(localeParams[0]);
335        } else if (localeParams.length == 2) {
336            return new Locale(localeParams[0], localeParams[1]);
337        } else if (localeParams.length == 3) {
338            return new Locale(localeParams[0], localeParams[1], localeParams[2]);
339        }
340        return null;
341    }
342
343    private static int hashCodeInternal(String locale, String mode, String extraValue,
344            boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype) {
345        return Arrays.hashCode(new Object[] {locale, mode, extraValue, isAuxiliary,
346                overridesImplicitlyEnabledSubtype});
347    }
348
349    /**
350     * Sort the list of InputMethodSubtype
351     * @param context Context will be used for getting localized strings from IME
352     * @param flags Flags for the sort order
353     * @param imi InputMethodInfo of which subtypes are subject to be sorted
354     * @param subtypeList List of InputMethodSubtype which will be sorted
355     * @return Sorted list of subtypes
356     * @hide
357     */
358    public static List<InputMethodSubtype> sort(Context context, int flags, InputMethodInfo imi,
359            List<InputMethodSubtype> subtypeList) {
360        if (imi == null) return subtypeList;
361        final HashSet<InputMethodSubtype> inputSubtypesSet = new HashSet<InputMethodSubtype>(
362                subtypeList);
363        final ArrayList<InputMethodSubtype> sortedList = new ArrayList<InputMethodSubtype>();
364        int N = imi.getSubtypeCount();
365        for (int i = 0; i < N; ++i) {
366            InputMethodSubtype subtype = imi.getSubtypeAt(i);
367            if (inputSubtypesSet.contains(subtype)) {
368                sortedList.add(subtype);
369                inputSubtypesSet.remove(subtype);
370            }
371        }
372        // If subtypes in inputSubtypesSet remain, that means these subtypes are not
373        // contained in imi, so the remaining subtypes will be appended.
374        for (InputMethodSubtype subtype: inputSubtypesSet) {
375            sortedList.add(subtype);
376        }
377        return sortedList;
378    }
379}
380