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