InputMethodInfo.java revision 5df0631003392c416c9617458dc2814f9eaec317
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;
39import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
40
41import java.io.IOException;
42import java.util.ArrayList;
43import java.util.List;
44import java.util.Map;
45
46/**
47 * This class is used to specify meta information of an input method.
48 *
49 * <p>It should be defined in an XML resource file with an {@code &lt;input-method>} element.
50 * For more information, see the guide to
51 * <a href="{@docRoot}guide/topics/text/creating-input-method.html">
52 * Creating an Input Method</a>.</p>
53 *
54 * @see InputMethodSubtype
55 *
56 * @attr ref android.R.styleable#InputMethod_settingsActivity
57 * @attr ref android.R.styleable#InputMethod_isDefault
58 * @attr ref android.R.styleable#InputMethod_supportsSwitchingToNextInputMethod
59 */
60public final class InputMethodInfo implements Parcelable {
61    static final String TAG = "InputMethodInfo";
62
63    /**
64     * The Service that implements this input method component.
65     */
66    final ResolveInfo mService;
67
68    /**
69     * The unique string Id to identify the input method.  This is generated
70     * from the input method component.
71     */
72    final String mId;
73
74    /**
75     * The input method setting activity's name, used by the system settings to
76     * launch the setting activity of this input method.
77     */
78    final String mSettingsActivityName;
79
80    /**
81     * The resource in the input method's .apk that holds a boolean indicating
82     * whether it should be considered the default input method for this
83     * system.  This is a resource ID instead of the final value so that it
84     * can change based on the configuration (in particular locale).
85     */
86    final int mIsDefaultResId;
87
88    /**
89     * The array of the subtypes.
90     */
91    private final ArrayList<InputMethodSubtype> mSubtypes = new ArrayList<InputMethodSubtype>();
92
93    private final boolean mIsAuxIme;
94
95    /**
96     * Caveat: mForceDefault must be false for production. This flag is only for test.
97     */
98    private final boolean mForceDefault;
99
100    /**
101     * The flag whether this IME supports ways to switch to a next input method (e.g. globe key.)
102     */
103    private final boolean mSupportsSwitchingToNextInputMethod;
104
105    /**
106     * Constructor.
107     *
108     * @param context The Context in which we are parsing the input method.
109     * @param service The ResolveInfo returned from the package manager about
110     * this input method's component.
111     */
112    public InputMethodInfo(Context context, ResolveInfo service)
113            throws XmlPullParserException, IOException {
114        this(context, service, null);
115    }
116
117    /**
118     * Constructor.
119     *
120     * @param context The Context in which we are parsing the input method.
121     * @param service The ResolveInfo returned from the package manager about
122     * this input method's component.
123     * @param additionalSubtypes additional subtypes being added to this InputMethodInfo
124     * @hide
125     */
126    public InputMethodInfo(Context context, ResolveInfo service,
127            Map<String, List<InputMethodSubtype>> additionalSubtypesMap)
128            throws XmlPullParserException, IOException {
129        mService = service;
130        ServiceInfo si = service.serviceInfo;
131        mId = new ComponentName(si.packageName, si.name).flattenToShortString();
132        boolean isAuxIme = true;
133        boolean supportsSwitchingToNextInputMethod = false; // false as default
134        mForceDefault = false;
135
136        PackageManager pm = context.getPackageManager();
137        String settingsActivityComponent = null;
138        int isDefaultResId = 0;
139
140        XmlResourceParser parser = null;
141        try {
142            parser = si.loadXmlMetaData(pm, InputMethod.SERVICE_META_DATA);
143            if (parser == null) {
144                throw new XmlPullParserException("No "
145                        + InputMethod.SERVICE_META_DATA + " meta-data");
146            }
147
148            Resources res = pm.getResourcesForApplication(si.applicationInfo);
149
150            AttributeSet attrs = Xml.asAttributeSet(parser);
151
152            int type;
153            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
154                    && type != XmlPullParser.START_TAG) {
155            }
156
157            String nodeName = parser.getName();
158            if (!"input-method".equals(nodeName)) {
159                throw new XmlPullParserException(
160                        "Meta-data does not start with input-method tag");
161            }
162
163            TypedArray sa = res.obtainAttributes(attrs,
164                    com.android.internal.R.styleable.InputMethod);
165            settingsActivityComponent = sa.getString(
166                    com.android.internal.R.styleable.InputMethod_settingsActivity);
167            isDefaultResId = sa.getResourceId(
168                    com.android.internal.R.styleable.InputMethod_isDefault, 0);
169            supportsSwitchingToNextInputMethod = sa.getBoolean(
170                    com.android.internal.R.styleable.InputMethod_supportsSwitchingToNextInputMethod,
171                    false);
172            sa.recycle();
173
174            final int depth = parser.getDepth();
175            // Parse all subtypes
176            while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
177                    && type != XmlPullParser.END_DOCUMENT) {
178                if (type == XmlPullParser.START_TAG) {
179                    nodeName = parser.getName();
180                    if (!"subtype".equals(nodeName)) {
181                        throw new XmlPullParserException(
182                                "Meta-data in input-method does not start with subtype tag");
183                    }
184                    final TypedArray a = res.obtainAttributes(
185                            attrs, com.android.internal.R.styleable.InputMethod_Subtype);
186                    final InputMethodSubtype subtype = new InputMethodSubtypeBuilder()
187                            .setSubtypeNameResId(a.getResourceId(com.android.internal.R.styleable
188                                    .InputMethod_Subtype_label, 0))
189                            .setSubtypeIconResId(a.getResourceId(com.android.internal.R.styleable
190                                    .InputMethod_Subtype_icon, 0))
191                            .setSubtypeLocale(a.getString(com.android.internal.R.styleable
192                                    .InputMethod_Subtype_imeSubtypeLocale))
193                            .setSubtypeMode(a.getString(com.android.internal.R.styleable
194                                    .InputMethod_Subtype_imeSubtypeMode))
195                            .setSubtypeExtraValue(a.getString(com.android.internal.R.styleable
196                                    .InputMethod_Subtype_imeSubtypeExtraValue))
197                            .setIsAuxiliary(a.getBoolean(com.android.internal.R.styleable
198                                    .InputMethod_Subtype_isAuxiliary, false))
199                            .setOverridesImplicitlyEnabledSubtype(a.getBoolean(
200                                    com.android.internal.R.styleable
201                                    .InputMethod_Subtype_overridesImplicitlyEnabledSubtype, false))
202                            .setSubtypeId(a.getInt(com.android.internal.R.styleable
203                                    .InputMethod_Subtype_subtypeId, 0 /* use Arrays.hashCode */))
204                            .setIsAsciiCapable(a.getBoolean(com.android.internal.R.styleable
205                                    .InputMethod_Subtype_isAsciiCapable, false)).build();
206                    if (!subtype.isAuxiliary()) {
207                        isAuxIme = false;
208                    }
209                    mSubtypes.add(subtype);
210                }
211            }
212        } catch (NameNotFoundException e) {
213            throw new XmlPullParserException(
214                    "Unable to create context for: " + si.packageName);
215        } finally {
216            if (parser != null) parser.close();
217        }
218
219        if (mSubtypes.size() == 0) {
220            isAuxIme = false;
221        }
222
223        if (additionalSubtypesMap != null && additionalSubtypesMap.containsKey(mId)) {
224            final List<InputMethodSubtype> additionalSubtypes = additionalSubtypesMap.get(mId);
225            final int N = additionalSubtypes.size();
226            for (int i = 0; i < N; ++i) {
227                final InputMethodSubtype subtype = additionalSubtypes.get(i);
228                if (!mSubtypes.contains(subtype)) {
229                    mSubtypes.add(subtype);
230                } else {
231                    Slog.w(TAG, "Duplicated subtype definition found: "
232                            + subtype.getLocale() + ", " + subtype.getMode());
233                }
234            }
235        }
236        mSettingsActivityName = settingsActivityComponent;
237        mIsDefaultResId = isDefaultResId;
238        mIsAuxIme = isAuxIme;
239        mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod;
240    }
241
242    InputMethodInfo(Parcel source) {
243        mId = source.readString();
244        mSettingsActivityName = source.readString();
245        mIsDefaultResId = source.readInt();
246        mIsAuxIme = source.readInt() == 1;
247        mSupportsSwitchingToNextInputMethod = source.readInt() == 1;
248        mService = ResolveInfo.CREATOR.createFromParcel(source);
249        source.readTypedList(mSubtypes, InputMethodSubtype.CREATOR);
250        mForceDefault = false;
251    }
252
253    /**
254     * Temporary API for creating a built-in input method for test.
255     */
256    public InputMethodInfo(String packageName, String className,
257            CharSequence label, String settingsActivity) {
258        this(buildDummyResolveInfo(packageName, className, label), false, settingsActivity, null,
259                0, false);
260    }
261
262    /**
263     * Temporary API for creating a built-in input method for test.
264     * @hide
265     */
266    public InputMethodInfo(ResolveInfo ri, boolean isAuxIme,
267            String settingsActivity, List<InputMethodSubtype> subtypes, int isDefaultResId,
268            boolean forceDefault) {
269        final ServiceInfo si = ri.serviceInfo;
270        mService = ri;
271        mId = new ComponentName(si.packageName, si.name).flattenToShortString();
272        mSettingsActivityName = settingsActivity;
273        mIsDefaultResId = isDefaultResId;
274        mIsAuxIme = isAuxIme;
275        if (subtypes != null) {
276            mSubtypes.addAll(subtypes);
277        }
278        mForceDefault = forceDefault;
279        mSupportsSwitchingToNextInputMethod = true;
280    }
281
282    private static ResolveInfo buildDummyResolveInfo(String packageName, String className,
283            CharSequence label) {
284        ResolveInfo ri = new ResolveInfo();
285        ServiceInfo si = new ServiceInfo();
286        ApplicationInfo ai = new ApplicationInfo();
287        ai.packageName = packageName;
288        ai.enabled = true;
289        si.applicationInfo = ai;
290        si.enabled = true;
291        si.packageName = packageName;
292        si.name = className;
293        si.exported = true;
294        si.nonLocalizedLabel = label;
295        ri.serviceInfo = si;
296        return ri;
297    }
298
299    /**
300     * Return a unique ID for this input method.  The ID is generated from
301     * the package and class name implementing the method.
302     */
303    public String getId() {
304        return mId;
305    }
306
307    /**
308     * Return the .apk package that implements this input method.
309     */
310    public String getPackageName() {
311        return mService.serviceInfo.packageName;
312    }
313
314    /**
315     * Return the class name of the service component that implements
316     * this input method.
317     */
318    public String getServiceName() {
319        return mService.serviceInfo.name;
320    }
321
322    /**
323     * Return the raw information about the Service implementing this
324     * input method.  Do not modify the returned object.
325     */
326    public ServiceInfo getServiceInfo() {
327        return mService.serviceInfo;
328    }
329
330    /**
331     * Return the component of the service that implements this input
332     * method.
333     */
334    public ComponentName getComponent() {
335        return new ComponentName(mService.serviceInfo.packageName,
336                mService.serviceInfo.name);
337    }
338
339    /**
340     * Load the user-displayed label for this input method.
341     *
342     * @param pm Supply a PackageManager used to load the input method's
343     * resources.
344     */
345    public CharSequence loadLabel(PackageManager pm) {
346        return mService.loadLabel(pm);
347    }
348
349    /**
350     * Load the user-displayed icon for this input method.
351     *
352     * @param pm Supply a PackageManager used to load the input method's
353     * resources.
354     */
355    public Drawable loadIcon(PackageManager pm) {
356        return mService.loadIcon(pm);
357    }
358
359    /**
360     * Return the class name of an activity that provides a settings UI for
361     * the input method.  You can launch this activity be starting it with
362     * an {@link android.content.Intent} whose action is MAIN and with an
363     * explicit {@link android.content.ComponentName}
364     * composed of {@link #getPackageName} and the class name returned here.
365     *
366     * <p>A null will be returned if there is no settings activity associated
367     * with the input method.
368     */
369    public String getSettingsActivity() {
370        return mSettingsActivityName;
371    }
372
373    /**
374     * Return the count of the subtypes of Input Method.
375     */
376    public int getSubtypeCount() {
377        return mSubtypes.size();
378    }
379
380    /**
381     * Return the Input Method's subtype at the specified index.
382     *
383     * @param index the index of the subtype to return.
384     */
385    public InputMethodSubtype getSubtypeAt(int index) {
386        return mSubtypes.get(index);
387    }
388
389    /**
390     * Return the resource identifier of a resource inside of this input
391     * method's .apk that determines whether it should be considered a
392     * default input method for the system.
393     */
394    public int getIsDefaultResourceId() {
395        return mIsDefaultResId;
396    }
397
398    /**
399     * Return whether or not this ime is a default ime or not.
400     * @hide
401     */
402    public boolean isDefault(Context context) {
403        if (mForceDefault) {
404            return true;
405        }
406        try {
407            final Resources res = context.createPackageContext(getPackageName(), 0).getResources();
408            return res.getBoolean(getIsDefaultResourceId());
409        } catch (NameNotFoundException e) {
410            return false;
411        }
412    }
413
414    public void dump(Printer pw, String prefix) {
415        pw.println(prefix + "mId=" + mId
416                + " mSettingsActivityName=" + mSettingsActivityName);
417        pw.println(prefix + "mIsDefaultResId=0x"
418                + Integer.toHexString(mIsDefaultResId));
419        pw.println(prefix + "Service:");
420        mService.dump(pw, prefix + "  ");
421    }
422
423    @Override
424    public String toString() {
425        return "InputMethodInfo{" + mId
426                + ", settings: "
427                + mSettingsActivityName + "}";
428    }
429
430    /**
431     * Used to test whether the given parameter object is an
432     * {@link InputMethodInfo} and its Id is the same to this one.
433     *
434     * @return true if the given parameter object is an
435     *         {@link InputMethodInfo} and its Id is the same to this one.
436     */
437    @Override
438    public boolean equals(Object o) {
439        if (o == this) return true;
440        if (o == null) return false;
441
442        if (!(o instanceof InputMethodInfo)) return false;
443
444        InputMethodInfo obj = (InputMethodInfo) o;
445        return mId.equals(obj.mId);
446    }
447
448    @Override
449    public int hashCode() {
450        return mId.hashCode();
451    }
452
453    /**
454     * @hide
455     */
456    public boolean isAuxiliaryIme() {
457        return mIsAuxIme;
458    }
459
460    /**
461     * @return true if this input method supports ways to switch to a next input method.
462     * @hide
463     */
464    public boolean supportsSwitchingToNextInputMethod() {
465        return mSupportsSwitchingToNextInputMethod;
466    }
467
468    /**
469     * Used to package this object into a {@link Parcel}.
470     *
471     * @param dest The {@link Parcel} to be written.
472     * @param flags The flags used for parceling.
473     */
474    @Override
475    public void writeToParcel(Parcel dest, int flags) {
476        dest.writeString(mId);
477        dest.writeString(mSettingsActivityName);
478        dest.writeInt(mIsDefaultResId);
479        dest.writeInt(mIsAuxIme ? 1 : 0);
480        dest.writeInt(mSupportsSwitchingToNextInputMethod ? 1 : 0);
481        mService.writeToParcel(dest, flags);
482        dest.writeTypedList(mSubtypes);
483    }
484
485    /**
486     * Used to make this class parcelable.
487     */
488    public static final Parcelable.Creator<InputMethodInfo> CREATOR
489            = new Parcelable.Creator<InputMethodInfo>() {
490        @Override
491        public InputMethodInfo createFromParcel(Parcel source) {
492            return new InputMethodInfo(source);
493        }
494
495        @Override
496        public InputMethodInfo[] newArray(int size) {
497            return new InputMethodInfo[size];
498        }
499    };
500
501    @Override
502    public int describeContents() {
503        return 0;
504    }
505}
506