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