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