1/*
2 * Copyright (C) 2007-2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package android.view.inputmethod;
18
19import org.xmlpull.v1.XmlPullParser;
20import org.xmlpull.v1.XmlPullParserException;
21
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.pm.ApplicationInfo;
25import android.content.pm.PackageManager;
26import android.content.pm.PackageManager.NameNotFoundException;
27import android.content.pm.ResolveInfo;
28import android.content.pm.ServiceInfo;
29import android.content.res.Resources;
30import android.content.res.TypedArray;
31import android.content.res.XmlResourceParser;
32import android.graphics.drawable.Drawable;
33import android.os.Parcel;
34import android.os.Parcelable;
35import android.util.AttributeSet;
36import android.util.Printer;
37import android.util.Slog;
38import android.util.Xml;
39
40import java.io.IOException;
41import java.util.ArrayList;
42import java.util.List;
43import java.util.Map;
44
45/**
46 * This class is used to specify meta information of an input method.
47 */
48public final class InputMethodInfo implements Parcelable {
49    static final String TAG = "InputMethodInfo";
50
51    /**
52     * The Service that implements this input method component.
53     */
54    final ResolveInfo mService;
55
56    /**
57     * The unique string Id to identify the input method.  This is generated
58     * from the input method component.
59     */
60    final String mId;
61
62    /**
63     * The input method setting activity's name, used by the system settings to
64     * launch the setting activity of this input method.
65     */
66    final String mSettingsActivityName;
67
68    /**
69     * The resource in the input method's .apk that holds a boolean indicating
70     * whether it should be considered the default input method for this
71     * system.  This is a resource ID instead of the final value so that it
72     * can change based on the configuration (in particular locale).
73     */
74    final int mIsDefaultResId;
75
76    /**
77     * The array of the subtypes.
78     */
79    private final ArrayList<InputMethodSubtype> mSubtypes = new ArrayList<InputMethodSubtype>();
80
81    private boolean mIsAuxIme;
82
83    /**
84     * Constructor.
85     *
86     * @param context The Context in which we are parsing the input method.
87     * @param service The ResolveInfo returned from the package manager about
88     * this input method's component.
89     */
90    public InputMethodInfo(Context context, ResolveInfo service)
91            throws XmlPullParserException, IOException {
92        this(context, service, null);
93    }
94
95    /**
96     * Constructor.
97     *
98     * @param context The Context in which we are parsing the input method.
99     * @param service The ResolveInfo returned from the package manager about
100     * this input method's component.
101     * @param additionalSubtypes additional subtypes being added to this InputMethodInfo
102     * @hide
103     */
104    public InputMethodInfo(Context context, ResolveInfo service,
105            Map<String, List<InputMethodSubtype>> additionalSubtypesMap)
106            throws XmlPullParserException, IOException {
107        mService = service;
108        ServiceInfo si = service.serviceInfo;
109        mId = new ComponentName(si.packageName, si.name).flattenToShortString();
110        mIsAuxIme = true;
111
112        PackageManager pm = context.getPackageManager();
113        String settingsActivityComponent = null;
114        int isDefaultResId = 0;
115
116        XmlResourceParser parser = null;
117        try {
118            parser = si.loadXmlMetaData(pm, InputMethod.SERVICE_META_DATA);
119            if (parser == null) {
120                throw new XmlPullParserException("No "
121                        + InputMethod.SERVICE_META_DATA + " meta-data");
122            }
123
124            Resources res = pm.getResourcesForApplication(si.applicationInfo);
125
126            AttributeSet attrs = Xml.asAttributeSet(parser);
127
128            int type;
129            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
130                    && type != XmlPullParser.START_TAG) {
131            }
132
133            String nodeName = parser.getName();
134            if (!"input-method".equals(nodeName)) {
135                throw new XmlPullParserException(
136                        "Meta-data does not start with input-method tag");
137            }
138
139            TypedArray sa = res.obtainAttributes(attrs,
140                    com.android.internal.R.styleable.InputMethod);
141            settingsActivityComponent = sa.getString(
142                    com.android.internal.R.styleable.InputMethod_settingsActivity);
143            isDefaultResId = sa.getResourceId(
144                    com.android.internal.R.styleable.InputMethod_isDefault, 0);
145            sa.recycle();
146
147            final int depth = parser.getDepth();
148            // Parse all subtypes
149            while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
150                    && type != XmlPullParser.END_DOCUMENT) {
151                if (type == XmlPullParser.START_TAG) {
152                    nodeName = parser.getName();
153                    if (!"subtype".equals(nodeName)) {
154                        throw new XmlPullParserException(
155                                "Meta-data in input-method does not start with subtype tag");
156                    }
157                    final TypedArray a = res.obtainAttributes(
158                            attrs, com.android.internal.R.styleable.InputMethod_Subtype);
159                    InputMethodSubtype subtype = new InputMethodSubtype(
160                            a.getResourceId(com.android.internal.R.styleable
161                                    .InputMethod_Subtype_label, 0),
162                            a.getResourceId(com.android.internal.R.styleable
163                                    .InputMethod_Subtype_icon, 0),
164                            a.getString(com.android.internal.R.styleable
165                                    .InputMethod_Subtype_imeSubtypeLocale),
166                            a.getString(com.android.internal.R.styleable
167                                    .InputMethod_Subtype_imeSubtypeMode),
168                            a.getString(com.android.internal.R.styleable
169                                    .InputMethod_Subtype_imeSubtypeExtraValue),
170                            a.getBoolean(com.android.internal.R.styleable
171                                    .InputMethod_Subtype_isAuxiliary, false),
172                            a.getBoolean(com.android.internal.R.styleable
173                                    .InputMethod_Subtype_overridesImplicitlyEnabledSubtype, false),
174                            a.getInt(com.android.internal.R.styleable
175                                    .InputMethod_Subtype_subtypeId, 0 /* use Arrays.hashCode */)
176                            );
177                    if (!subtype.isAuxiliary()) {
178                        mIsAuxIme = false;
179                    }
180                    mSubtypes.add(subtype);
181                }
182            }
183        } catch (NameNotFoundException e) {
184            throw new XmlPullParserException(
185                    "Unable to create context for: " + si.packageName);
186        } finally {
187            if (parser != null) parser.close();
188        }
189
190        if (mSubtypes.size() == 0) {
191            mIsAuxIme = false;
192        }
193
194        if (additionalSubtypesMap != null && additionalSubtypesMap.containsKey(mId)) {
195            final List<InputMethodSubtype> additionalSubtypes = additionalSubtypesMap.get(mId);
196            final int N = additionalSubtypes.size();
197            for (int i = 0; i < N; ++i) {
198                final InputMethodSubtype subtype = additionalSubtypes.get(i);
199                if (!mSubtypes.contains(subtype)) {
200                    mSubtypes.add(subtype);
201                } else {
202                    Slog.w(TAG, "Duplicated subtype definition found: "
203                            + subtype.getLocale() + ", " + subtype.getMode());
204                }
205            }
206        }
207        mSettingsActivityName = settingsActivityComponent;
208        mIsDefaultResId = isDefaultResId;
209    }
210
211    InputMethodInfo(Parcel source) {
212        mId = source.readString();
213        mSettingsActivityName = source.readString();
214        mIsDefaultResId = source.readInt();
215        mIsAuxIme = source.readInt() == 1;
216        mService = ResolveInfo.CREATOR.createFromParcel(source);
217        source.readTypedList(mSubtypes, InputMethodSubtype.CREATOR);
218    }
219
220    /**
221     * Temporary API for creating a built-in input method.
222     */
223    public InputMethodInfo(String packageName, String className,
224            CharSequence label, String settingsActivity) {
225        ResolveInfo ri = new ResolveInfo();
226        ServiceInfo si = new ServiceInfo();
227        ApplicationInfo ai = new ApplicationInfo();
228        ai.packageName = packageName;
229        ai.enabled = true;
230        si.applicationInfo = ai;
231        si.enabled = true;
232        si.packageName = packageName;
233        si.name = className;
234        si.exported = true;
235        si.nonLocalizedLabel = label;
236        ri.serviceInfo = si;
237        mService = ri;
238        mId = new ComponentName(si.packageName, si.name).flattenToShortString();
239        mSettingsActivityName = settingsActivity;
240        mIsDefaultResId = 0;
241        mIsAuxIme = false;
242    }
243
244    /**
245     * Return a unique ID for this input method.  The ID is generated from
246     * the package and class name implementing the method.
247     */
248    public String getId() {
249        return mId;
250    }
251
252    /**
253     * Return the .apk package that implements this input method.
254     */
255    public String getPackageName() {
256        return mService.serviceInfo.packageName;
257    }
258
259    /**
260     * Return the class name of the service component that implements
261     * this input method.
262     */
263    public String getServiceName() {
264        return mService.serviceInfo.name;
265    }
266
267    /**
268     * Return the raw information about the Service implementing this
269     * input method.  Do not modify the returned object.
270     */
271    public ServiceInfo getServiceInfo() {
272        return mService.serviceInfo;
273    }
274
275    /**
276     * Return the component of the service that implements this input
277     * method.
278     */
279    public ComponentName getComponent() {
280        return new ComponentName(mService.serviceInfo.packageName,
281                mService.serviceInfo.name);
282    }
283
284    /**
285     * Load the user-displayed label for this input method.
286     *
287     * @param pm Supply a PackageManager used to load the input method's
288     * resources.
289     */
290    public CharSequence loadLabel(PackageManager pm) {
291        return mService.loadLabel(pm);
292    }
293
294    /**
295     * Load the user-displayed icon for this input method.
296     *
297     * @param pm Supply a PackageManager used to load the input method's
298     * resources.
299     */
300    public Drawable loadIcon(PackageManager pm) {
301        return mService.loadIcon(pm);
302    }
303
304    /**
305     * Return the class name of an activity that provides a settings UI for
306     * the input method.  You can launch this activity be starting it with
307     * an {@link android.content.Intent} whose action is MAIN and with an
308     * explicit {@link android.content.ComponentName}
309     * composed of {@link #getPackageName} and the class name returned here.
310     *
311     * <p>A null will be returned if there is no settings activity associated
312     * with the input method.
313     */
314    public String getSettingsActivity() {
315        return mSettingsActivityName;
316    }
317
318    /**
319     * Return the count of the subtypes of Input Method.
320     */
321    public int getSubtypeCount() {
322        return mSubtypes.size();
323    }
324
325    /**
326     * Return the Input Method's subtype at the specified index.
327     *
328     * @param index the index of the subtype to return.
329     */
330    public InputMethodSubtype getSubtypeAt(int index) {
331        return mSubtypes.get(index);
332    }
333
334    /**
335     * Return the resource identifier of a resource inside of this input
336     * method's .apk that determines whether it should be considered a
337     * default input method for the system.
338     */
339    public int getIsDefaultResourceId() {
340        return mIsDefaultResId;
341    }
342
343    public void dump(Printer pw, String prefix) {
344        pw.println(prefix + "mId=" + mId
345                + " mSettingsActivityName=" + mSettingsActivityName);
346        pw.println(prefix + "mIsDefaultResId=0x"
347                + Integer.toHexString(mIsDefaultResId));
348        pw.println(prefix + "Service:");
349        mService.dump(pw, prefix + "  ");
350    }
351
352    @Override
353    public String toString() {
354        return "InputMethodInfo{" + mId
355                + ", settings: "
356                + mSettingsActivityName + "}";
357    }
358
359    /**
360     * Used to test whether the given parameter object is an
361     * {@link InputMethodInfo} and its Id is the same to this one.
362     *
363     * @return true if the given parameter object is an
364     *         {@link InputMethodInfo} and its Id is the same to this one.
365     */
366    @Override
367    public boolean equals(Object o) {
368        if (o == this) return true;
369        if (o == null) return false;
370
371        if (!(o instanceof InputMethodInfo)) return false;
372
373        InputMethodInfo obj = (InputMethodInfo) o;
374        return mId.equals(obj.mId);
375    }
376
377    @Override
378    public int hashCode() {
379        return mId.hashCode();
380    }
381
382    /**
383     * @hide
384     */
385    public boolean isAuxiliaryIme() {
386        return mIsAuxIme;
387    }
388
389    /**
390     * Used to package this object into a {@link Parcel}.
391     *
392     * @param dest The {@link Parcel} to be written.
393     * @param flags The flags used for parceling.
394     */
395    @Override
396    public void writeToParcel(Parcel dest, int flags) {
397        dest.writeString(mId);
398        dest.writeString(mSettingsActivityName);
399        dest.writeInt(mIsDefaultResId);
400        dest.writeInt(mIsAuxIme ? 1 : 0);
401        mService.writeToParcel(dest, flags);
402        dest.writeTypedList(mSubtypes);
403    }
404
405    /**
406     * Used to make this class parcelable.
407     */
408    public static final Parcelable.Creator<InputMethodInfo> CREATOR
409            = new Parcelable.Creator<InputMethodInfo>() {
410        @Override
411        public InputMethodInfo createFromParcel(Parcel source) {
412            return new InputMethodInfo(source);
413        }
414
415        @Override
416        public InputMethodInfo[] newArray(int size) {
417            return new InputMethodInfo[size];
418        }
419    };
420
421    @Override
422    public int describeContents() {
423        return 0;
424    }
425}
426