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