1/*
2 * Copyright (C) 2011 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.textservice;
18
19import com.android.internal.inputmethod.InputMethodUtils;
20
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.content.Context;
24import android.content.pm.ApplicationInfo;
25import android.os.Parcel;
26import android.os.Parcelable;
27import android.text.TextUtils;
28import android.util.Slog;
29
30import java.util.ArrayList;
31import java.util.Arrays;
32import java.util.HashMap;
33import java.util.HashSet;
34import java.util.List;
35import java.util.Locale;
36
37/**
38 * This class is used to specify meta information of a subtype contained in a spell checker.
39 * Subtype can describe locale (e.g. en_US, fr_FR...) used for settings.
40 *
41 * @see SpellCheckerInfo
42 *
43 * @attr ref android.R.styleable#SpellChecker_Subtype_label
44 * @attr ref android.R.styleable#SpellChecker_Subtype_languageTag
45 * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeLocale
46 * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeExtraValue
47 * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeId
48 */
49public final class SpellCheckerSubtype implements Parcelable {
50    private static final String TAG = SpellCheckerSubtype.class.getSimpleName();
51    private static final String EXTRA_VALUE_PAIR_SEPARATOR = ",";
52    private static final String EXTRA_VALUE_KEY_VALUE_SEPARATOR = "=";
53    /**
54     * @hide
55     */
56    public static final int SUBTYPE_ID_NONE = 0;
57    private static final String SUBTYPE_LANGUAGE_TAG_NONE = "";
58
59    private final int mSubtypeId;
60    private final int mSubtypeHashCode;
61    private final int mSubtypeNameResId;
62    private final String mSubtypeLocale;
63    private final String mSubtypeLanguageTag;
64    private final String mSubtypeExtraValue;
65    private HashMap<String, String> mExtraValueHashMapCache;
66
67    /**
68     * Constructor.
69     *
70     * <p>There is no public API that requires developers to instantiate custom
71     * {@link SpellCheckerSubtype} object.  Hence so far there is no need to make this constructor
72     * available in public API.</p>
73     *
74     * @param nameId The name of the subtype
75     * @param locale The locale supported by the subtype
76     * @param languageTag The BCP-47 Language Tag associated with this subtype.
77     * @param extraValue The extra value of the subtype
78     * @param subtypeId The subtype ID that is supposed to be stable during package update.
79     *
80     * @hide
81     */
82    public SpellCheckerSubtype(int nameId, String locale, String languageTag, String extraValue,
83            int subtypeId) {
84        mSubtypeNameResId = nameId;
85        mSubtypeLocale = locale != null ? locale : "";
86        mSubtypeLanguageTag = languageTag != null ? languageTag : SUBTYPE_LANGUAGE_TAG_NONE;
87        mSubtypeExtraValue = extraValue != null ? extraValue : "";
88        mSubtypeId = subtypeId;
89        mSubtypeHashCode = mSubtypeId != SUBTYPE_ID_NONE ?
90                mSubtypeId : hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
91    }
92
93    /**
94     * Constructor.
95     * @param nameId The name of the subtype
96     * @param locale The locale supported by the subtype
97     * @param extraValue The extra value of the subtype
98     *
99     * @deprecated There is no public API that requires developers to directly instantiate custom
100     * {@link SpellCheckerSubtype} objects right now.  Hence only the system is expected to be able
101     * to instantiate {@link SpellCheckerSubtype} object.
102     */
103    public SpellCheckerSubtype(int nameId, String locale, String extraValue) {
104        this(nameId, locale, SUBTYPE_LANGUAGE_TAG_NONE, extraValue, SUBTYPE_ID_NONE);
105    }
106
107    SpellCheckerSubtype(Parcel source) {
108        String s;
109        mSubtypeNameResId = source.readInt();
110        s = source.readString();
111        mSubtypeLocale = s != null ? s : "";
112        s = source.readString();
113        mSubtypeLanguageTag = s != null ? s : "";
114        s = source.readString();
115        mSubtypeExtraValue = s != null ? s : "";
116        mSubtypeId = source.readInt();
117        mSubtypeHashCode = mSubtypeId != SUBTYPE_ID_NONE ?
118                mSubtypeId : hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
119    }
120
121    /**
122     * @return the name of the subtype
123     */
124    public int getNameResId() {
125        return mSubtypeNameResId;
126    }
127
128    /**
129     * @return the locale of the subtype
130     *
131     * @deprecated Use {@link #getLanguageTag()} instead.
132     */
133    @Deprecated
134    @NonNull
135    public String getLocale() {
136        return mSubtypeLocale;
137    }
138
139    /**
140     * @return the BCP-47 Language Tag of the subtype.  Returns an empty string when no Language Tag
141     * is specified.
142     *
143     * @see Locale#forLanguageTag(String)
144     */
145    @NonNull
146    public String getLanguageTag() {
147        return mSubtypeLanguageTag;
148    }
149
150    /**
151     * @return the extra value of the subtype
152     */
153    public String getExtraValue() {
154        return mSubtypeExtraValue;
155    }
156
157    private HashMap<String, String> getExtraValueHashMap() {
158        if (mExtraValueHashMapCache == null) {
159            mExtraValueHashMapCache = new HashMap<String, String>();
160            final String[] pairs = mSubtypeExtraValue.split(EXTRA_VALUE_PAIR_SEPARATOR);
161            final int N = pairs.length;
162            for (int i = 0; i < N; ++i) {
163                final String[] pair = pairs[i].split(EXTRA_VALUE_KEY_VALUE_SEPARATOR);
164                if (pair.length == 1) {
165                    mExtraValueHashMapCache.put(pair[0], null);
166                } else if (pair.length > 1) {
167                    if (pair.length > 2) {
168                        Slog.w(TAG, "ExtraValue has two or more '='s");
169                    }
170                    mExtraValueHashMapCache.put(pair[0], pair[1]);
171                }
172            }
173        }
174        return mExtraValueHashMapCache;
175    }
176
177    /**
178     * The string of ExtraValue in subtype should be defined as follows:
179     * example: key0,key1=value1,key2,key3,key4=value4
180     * @param key the key of extra value
181     * @return the subtype contains specified the extra value
182     */
183    public boolean containsExtraValueKey(String key) {
184        return getExtraValueHashMap().containsKey(key);
185    }
186
187    /**
188     * The string of ExtraValue in subtype should be defined as follows:
189     * example: key0,key1=value1,key2,key3,key4=value4
190     * @param key the key of extra value
191     * @return the value of the specified key
192     */
193    public String getExtraValueOf(String key) {
194        return getExtraValueHashMap().get(key);
195    }
196
197    @Override
198    public int hashCode() {
199        return mSubtypeHashCode;
200    }
201
202    @Override
203    public boolean equals(Object o) {
204        if (o instanceof SpellCheckerSubtype) {
205            SpellCheckerSubtype subtype = (SpellCheckerSubtype) o;
206            if (subtype.mSubtypeId != SUBTYPE_ID_NONE || mSubtypeId != SUBTYPE_ID_NONE) {
207                return (subtype.hashCode() == hashCode());
208            }
209            return (subtype.hashCode() == hashCode())
210                    && (subtype.getNameResId() == getNameResId())
211                    && (subtype.getLocale().equals(getLocale()))
212                    && (subtype.getLanguageTag().equals(getLanguageTag()))
213                    && (subtype.getExtraValue().equals(getExtraValue()));
214        }
215        return false;
216    }
217
218    /**
219     * @return {@link Locale} constructed from {@link #getLanguageTag()}. If the Language Tag is not
220     * specified, then try to construct from {@link #getLocale()}
221     *
222     * <p>TODO: Consider to make this a public API, or move this to support lib.</p>
223     * @hide
224     */
225    @Nullable
226    public Locale getLocaleObject() {
227        if (!TextUtils.isEmpty(mSubtypeLanguageTag)) {
228            return Locale.forLanguageTag(mSubtypeLanguageTag);
229        }
230        return InputMethodUtils.constructLocaleFromString(mSubtypeLocale);
231    }
232
233    /**
234     * @param context Context will be used for getting Locale and PackageManager.
235     * @param packageName The package name of the spell checker
236     * @param appInfo The application info of the spell checker
237     * @return a display name for this subtype. The string resource of the label (mSubtypeNameResId)
238     * can have only one %s in it. If there is, the %s part will be replaced with the locale's
239     * display name by the formatter. If there is not, this method simply returns the string
240     * specified by mSubtypeNameResId. If mSubtypeNameResId is not specified (== 0), it's up to the
241     * framework to generate an appropriate display name.
242     */
243    public CharSequence getDisplayName(
244            Context context, String packageName, ApplicationInfo appInfo) {
245        final Locale locale = getLocaleObject();
246        final String localeStr = locale != null ? locale.getDisplayName() : mSubtypeLocale;
247        if (mSubtypeNameResId == 0) {
248            return localeStr;
249        }
250        final CharSequence subtypeName = context.getPackageManager().getText(
251                packageName, mSubtypeNameResId, appInfo);
252        if (!TextUtils.isEmpty(subtypeName)) {
253            return String.format(subtypeName.toString(), localeStr);
254        } else {
255            return localeStr;
256        }
257    }
258
259    @Override
260    public int describeContents() {
261        return 0;
262    }
263
264    @Override
265    public void writeToParcel(Parcel dest, int parcelableFlags) {
266        dest.writeInt(mSubtypeNameResId);
267        dest.writeString(mSubtypeLocale);
268        dest.writeString(mSubtypeLanguageTag);
269        dest.writeString(mSubtypeExtraValue);
270        dest.writeInt(mSubtypeId);
271    }
272
273    public static final Parcelable.Creator<SpellCheckerSubtype> CREATOR
274            = new Parcelable.Creator<SpellCheckerSubtype>() {
275        @Override
276        public SpellCheckerSubtype createFromParcel(Parcel source) {
277            return new SpellCheckerSubtype(source);
278        }
279
280        @Override
281        public SpellCheckerSubtype[] newArray(int size) {
282            return new SpellCheckerSubtype[size];
283        }
284    };
285
286    private static int hashCodeInternal(String locale, String extraValue) {
287        return Arrays.hashCode(new Object[] {locale, extraValue});
288    }
289
290    /**
291     * Sort the list of subtypes
292     * @param context Context will be used for getting localized strings
293     * @param flags Flags for the sort order
294     * @param sci SpellCheckerInfo of which subtypes are subject to be sorted
295     * @param subtypeList List which will be sorted
296     * @return Sorted list of subtypes
297     * @hide
298     */
299    public static List<SpellCheckerSubtype> sort(Context context, int flags, SpellCheckerInfo sci,
300            List<SpellCheckerSubtype> subtypeList) {
301        if (sci == null) return subtypeList;
302        final HashSet<SpellCheckerSubtype> subtypesSet = new HashSet<SpellCheckerSubtype>(
303                subtypeList);
304        final ArrayList<SpellCheckerSubtype> sortedList = new ArrayList<SpellCheckerSubtype>();
305        int N = sci.getSubtypeCount();
306        for (int i = 0; i < N; ++i) {
307            SpellCheckerSubtype subtype = sci.getSubtypeAt(i);
308            if (subtypesSet.contains(subtype)) {
309                sortedList.add(subtype);
310                subtypesSet.remove(subtype);
311            }
312        }
313        // If subtypes in subtypesSet remain, that means these subtypes are not
314        // contained in sci, so the remaining subtypes will be appended.
315        for (SpellCheckerSubtype subtype: subtypesSet) {
316            sortedList.add(subtype);
317        }
318        return sortedList;
319    }
320}
321