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