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