1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.nfc.cardemulation; 18 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.pm.PackageManager; 22 import android.content.pm.ResolveInfo; 23 import android.content.pm.ServiceInfo; 24 import android.content.pm.PackageManager.NameNotFoundException; 25 import android.content.res.Resources; 26 import android.content.res.Resources.NotFoundException; 27 import android.content.res.TypedArray; 28 import android.content.res.XmlResourceParser; 29 import android.graphics.drawable.Drawable; 30 import android.os.Parcel; 31 import android.os.Parcelable; 32 import android.os.ResultReceiver; 33 import android.util.AttributeSet; 34 import android.util.Log; 35 import android.util.Xml; 36 37 import org.xmlpull.v1.XmlPullParser; 38 import org.xmlpull.v1.XmlPullParserException; 39 40 import java.io.FileDescriptor; 41 import java.io.IOException; 42 import java.io.PrintWriter; 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 48 /** 49 * @hide 50 */ 51 public final class ApduServiceInfo implements Parcelable { 52 static final String TAG = "ApduServiceInfo"; 53 54 /** 55 * The service that implements this 56 */ 57 final ResolveInfo mService; 58 59 /** 60 * Description of the service 61 */ 62 final String mDescription; 63 64 /** 65 * Whether this service represents AIDs running on the host CPU 66 */ 67 final boolean mOnHost; 68 69 /** 70 * Mapping from category to static AID group 71 */ 72 final HashMap<String, AidGroup> mStaticAidGroups; 73 74 /** 75 * Mapping from category to dynamic AID group 76 */ 77 final HashMap<String, AidGroup> mDynamicAidGroups; 78 79 /** 80 * Whether this service should only be started when the device is unlocked. 81 */ 82 final boolean mRequiresDeviceUnlock; 83 84 /** 85 * The id of the service banner specified in XML. 86 */ 87 final int mBannerResourceId; 88 89 /** 90 * The uid of the package the service belongs to 91 */ 92 final int mUid; 93 94 /** 95 * Settings Activity for this service 96 */ 97 final String mSettingsActivityName; 98 99 /** 100 * @hide 101 */ 102 public ApduServiceInfo(ResolveInfo info, boolean onHost, String description, 103 ArrayList<AidGroup> staticAidGroups, ArrayList<AidGroup> dynamicAidGroups, 104 boolean requiresUnlock, int bannerResource, int uid, 105 String settingsActivityName) { 106 this.mService = info; 107 this.mDescription = description; 108 this.mStaticAidGroups = new HashMap<String, AidGroup>(); 109 this.mDynamicAidGroups = new HashMap<String, AidGroup>(); 110 this.mOnHost = onHost; 111 this.mRequiresDeviceUnlock = requiresUnlock; 112 for (AidGroup aidGroup : staticAidGroups) { 113 this.mStaticAidGroups.put(aidGroup.category, aidGroup); 114 } 115 for (AidGroup aidGroup : dynamicAidGroups) { 116 this.mDynamicAidGroups.put(aidGroup.category, aidGroup); 117 } 118 this.mBannerResourceId = bannerResource; 119 this.mUid = uid; 120 this.mSettingsActivityName = settingsActivityName; 121 } 122 123 public ApduServiceInfo(PackageManager pm, ResolveInfo info, boolean onHost) throws 124 XmlPullParserException, IOException { 125 ServiceInfo si = info.serviceInfo; 126 XmlResourceParser parser = null; 127 try { 128 if (onHost) { 129 parser = si.loadXmlMetaData(pm, HostApduService.SERVICE_META_DATA); 130 if (parser == null) { 131 throw new XmlPullParserException("No " + HostApduService.SERVICE_META_DATA + 132 " meta-data"); 133 } 134 } else { 135 parser = si.loadXmlMetaData(pm, OffHostApduService.SERVICE_META_DATA); 136 if (parser == null) { 137 throw new XmlPullParserException("No " + OffHostApduService.SERVICE_META_DATA + 138 " meta-data"); 139 } 140 } 141 142 int eventType = parser.getEventType(); 143 while (eventType != XmlPullParser.START_TAG && eventType != XmlPullParser.END_DOCUMENT) { 144 eventType = parser.next(); 145 } 146 147 String tagName = parser.getName(); 148 if (onHost && !"host-apdu-service".equals(tagName)) { 149 throw new XmlPullParserException( 150 "Meta-data does not start with <host-apdu-service> tag"); 151 } else if (!onHost && !"offhost-apdu-service".equals(tagName)) { 152 throw new XmlPullParserException( 153 "Meta-data does not start with <offhost-apdu-service> tag"); 154 } 155 156 Resources res = pm.getResourcesForApplication(si.applicationInfo); 157 AttributeSet attrs = Xml.asAttributeSet(parser); 158 if (onHost) { 159 TypedArray sa = res.obtainAttributes(attrs, 160 com.android.internal.R.styleable.HostApduService); 161 mService = info; 162 mDescription = sa.getString( 163 com.android.internal.R.styleable.HostApduService_description); 164 mRequiresDeviceUnlock = sa.getBoolean( 165 com.android.internal.R.styleable.HostApduService_requireDeviceUnlock, 166 false); 167 mBannerResourceId = sa.getResourceId( 168 com.android.internal.R.styleable.HostApduService_apduServiceBanner, -1); 169 mSettingsActivityName = sa.getString( 170 com.android.internal.R.styleable.HostApduService_settingsActivity); 171 sa.recycle(); 172 } else { 173 TypedArray sa = res.obtainAttributes(attrs, 174 com.android.internal.R.styleable.OffHostApduService); 175 mService = info; 176 mDescription = sa.getString( 177 com.android.internal.R.styleable.OffHostApduService_description); 178 mRequiresDeviceUnlock = false; 179 mBannerResourceId = sa.getResourceId( 180 com.android.internal.R.styleable.OffHostApduService_apduServiceBanner, -1); 181 mSettingsActivityName = sa.getString( 182 com.android.internal.R.styleable.HostApduService_settingsActivity); 183 sa.recycle(); 184 } 185 186 mStaticAidGroups = new HashMap<String, AidGroup>(); 187 mDynamicAidGroups = new HashMap<String, AidGroup>(); 188 mOnHost = onHost; 189 190 final int depth = parser.getDepth(); 191 AidGroup currentGroup = null; 192 193 // Parsed values for the current AID group 194 while (((eventType = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 195 && eventType != XmlPullParser.END_DOCUMENT) { 196 tagName = parser.getName(); 197 if (eventType == XmlPullParser.START_TAG && "aid-group".equals(tagName) && 198 currentGroup == null) { 199 final TypedArray groupAttrs = res.obtainAttributes(attrs, 200 com.android.internal.R.styleable.AidGroup); 201 // Get category of AID group 202 String groupCategory = groupAttrs.getString( 203 com.android.internal.R.styleable.AidGroup_category); 204 String groupDescription = groupAttrs.getString( 205 com.android.internal.R.styleable.AidGroup_description); 206 if (!CardEmulation.CATEGORY_PAYMENT.equals(groupCategory)) { 207 groupCategory = CardEmulation.CATEGORY_OTHER; 208 } 209 currentGroup = mStaticAidGroups.get(groupCategory); 210 if (currentGroup != null) { 211 if (!CardEmulation.CATEGORY_OTHER.equals(groupCategory)) { 212 Log.e(TAG, "Not allowing multiple aid-groups in the " + 213 groupCategory + " category"); 214 currentGroup = null; 215 } 216 } else { 217 currentGroup = new AidGroup(groupCategory, groupDescription); 218 } 219 groupAttrs.recycle(); 220 } else if (eventType == XmlPullParser.END_TAG && "aid-group".equals(tagName) && 221 currentGroup != null) { 222 if (currentGroup.aids.size() > 0) { 223 if (!mStaticAidGroups.containsKey(currentGroup.category)) { 224 mStaticAidGroups.put(currentGroup.category, currentGroup); 225 } 226 } else { 227 Log.e(TAG, "Not adding <aid-group> with empty or invalid AIDs"); 228 } 229 currentGroup = null; 230 } else if (eventType == XmlPullParser.START_TAG && "aid-filter".equals(tagName) && 231 currentGroup != null) { 232 final TypedArray a = res.obtainAttributes(attrs, 233 com.android.internal.R.styleable.AidFilter); 234 String aid = a.getString(com.android.internal.R.styleable.AidFilter_name). 235 toUpperCase(); 236 if (CardEmulation.isValidAid(aid) && !currentGroup.aids.contains(aid)) { 237 currentGroup.aids.add(aid); 238 } else { 239 Log.e(TAG, "Ignoring invalid or duplicate aid: " + aid); 240 } 241 a.recycle(); 242 } else if (eventType == XmlPullParser.START_TAG && 243 "aid-prefix-filter".equals(tagName) && currentGroup != null) { 244 final TypedArray a = res.obtainAttributes(attrs, 245 com.android.internal.R.styleable.AidFilter); 246 String aid = a.getString(com.android.internal.R.styleable.AidFilter_name). 247 toUpperCase(); 248 // Add wildcard char to indicate prefix 249 aid = aid.concat("*"); 250 if (CardEmulation.isValidAid(aid) && !currentGroup.aids.contains(aid)) { 251 currentGroup.aids.add(aid); 252 } else { 253 Log.e(TAG, "Ignoring invalid or duplicate aid: " + aid); 254 } 255 a.recycle(); 256 } else if (eventType == XmlPullParser.START_TAG && 257 tagName.equals("aid-suffix-filter") && currentGroup != null) { 258 final TypedArray a = res.obtainAttributes(attrs, 259 com.android.internal.R.styleable.AidFilter); 260 String aid = a.getString(com.android.internal.R.styleable.AidFilter_name). 261 toUpperCase(); 262 // Add wildcard char to indicate suffix 263 aid = aid.concat("#"); 264 if (CardEmulation.isValidAid(aid) && !currentGroup.aids.contains(aid)) { 265 currentGroup.aids.add(aid); 266 } else { 267 Log.e(TAG, "Ignoring invalid or duplicate aid: " + aid); 268 } 269 a.recycle(); 270 } 271 } 272 } catch (NameNotFoundException e) { 273 throw new XmlPullParserException("Unable to create context for: " + si.packageName); 274 } finally { 275 if (parser != null) parser.close(); 276 } 277 // Set uid 278 mUid = si.applicationInfo.uid; 279 } 280 281 public ComponentName getComponent() { 282 return new ComponentName(mService.serviceInfo.packageName, 283 mService.serviceInfo.name); 284 } 285 286 /** 287 * Returns a consolidated list of AIDs from the AID groups 288 * registered by this service. Note that if a service has both 289 * a static (manifest-based) AID group for a category and a dynamic 290 * AID group, only the dynamically registered AIDs will be returned 291 * for that category. 292 * @return List of AIDs registered by the service 293 */ 294 public List<String> getAids() { 295 final ArrayList<String> aids = new ArrayList<String>(); 296 for (AidGroup group : getAidGroups()) { 297 aids.addAll(group.aids); 298 } 299 return aids; 300 } 301 302 public List<String> getPrefixAids() { 303 final ArrayList<String> prefixAids = new ArrayList<String>(); 304 for (AidGroup group : getAidGroups()) { 305 for (String aid : group.aids) { 306 if (aid.endsWith("*")) { 307 prefixAids.add(aid); 308 } 309 } 310 } 311 return prefixAids; 312 } 313 314 public List<String> getSubsetAids() { 315 final ArrayList<String> subsetAids = new ArrayList<String>(); 316 for (AidGroup group : getAidGroups()) { 317 for (String aid : group.aids) { 318 if (aid.endsWith("#")) { 319 subsetAids.add(aid); 320 } 321 } 322 } 323 return subsetAids; 324 } 325 /** 326 * Returns the registered AID group for this category. 327 */ 328 public AidGroup getDynamicAidGroupForCategory(String category) { 329 return mDynamicAidGroups.get(category); 330 } 331 332 public boolean removeDynamicAidGroupForCategory(String category) { 333 return (mDynamicAidGroups.remove(category) != null); 334 } 335 336 /** 337 * Returns a consolidated list of AID groups 338 * registered by this service. Note that if a service has both 339 * a static (manifest-based) AID group for a category and a dynamic 340 * AID group, only the dynamically registered AID group will be returned 341 * for that category. 342 * @return List of AIDs registered by the service 343 */ 344 public ArrayList<AidGroup> getAidGroups() { 345 final ArrayList<AidGroup> groups = new ArrayList<AidGroup>(); 346 for (Map.Entry<String, AidGroup> entry : mDynamicAidGroups.entrySet()) { 347 groups.add(entry.getValue()); 348 } 349 for (Map.Entry<String, AidGroup> entry : mStaticAidGroups.entrySet()) { 350 if (!mDynamicAidGroups.containsKey(entry.getKey())) { 351 // Consolidate AID groups - don't return static ones 352 // if a dynamic group exists for the category. 353 groups.add(entry.getValue()); 354 } 355 } 356 return groups; 357 } 358 359 /** 360 * Returns the category to which this service has attributed the AID that is passed in, 361 * or null if we don't know this AID. 362 */ 363 public String getCategoryForAid(String aid) { 364 ArrayList<AidGroup> groups = getAidGroups(); 365 for (AidGroup group : groups) { 366 if (group.aids.contains(aid.toUpperCase())) { 367 return group.category; 368 } 369 } 370 return null; 371 } 372 373 public boolean hasCategory(String category) { 374 return (mStaticAidGroups.containsKey(category) || mDynamicAidGroups.containsKey(category)); 375 } 376 377 public boolean isOnHost() { 378 return mOnHost; 379 } 380 381 public boolean requiresUnlock() { 382 return mRequiresDeviceUnlock; 383 } 384 385 public String getDescription() { 386 return mDescription; 387 } 388 389 public int getUid() { 390 return mUid; 391 } 392 393 public void setOrReplaceDynamicAidGroup(AidGroup aidGroup) { 394 mDynamicAidGroups.put(aidGroup.getCategory(), aidGroup); 395 } 396 397 public CharSequence loadLabel(PackageManager pm) { 398 return mService.loadLabel(pm); 399 } 400 401 public CharSequence loadAppLabel(PackageManager pm) { 402 try { 403 return pm.getApplicationLabel(pm.getApplicationInfo( 404 mService.resolvePackageName, PackageManager.GET_META_DATA)); 405 } catch (PackageManager.NameNotFoundException e) { 406 return null; 407 } 408 } 409 410 public Drawable loadIcon(PackageManager pm) { 411 return mService.loadIcon(pm); 412 } 413 414 public Drawable loadBanner(PackageManager pm) { 415 Resources res; 416 try { 417 res = pm.getResourcesForApplication(mService.serviceInfo.packageName); 418 Drawable banner = res.getDrawable(mBannerResourceId); 419 return banner; 420 } catch (NotFoundException e) { 421 Log.e(TAG, "Could not load banner."); 422 return null; 423 } catch (NameNotFoundException e) { 424 Log.e(TAG, "Could not load banner."); 425 return null; 426 } 427 } 428 429 public String getSettingsActivityName() { return mSettingsActivityName; } 430 431 @Override 432 public String toString() { 433 StringBuilder out = new StringBuilder("ApduService: "); 434 out.append(getComponent()); 435 out.append(", description: " + mDescription); 436 out.append(", Static AID Groups: "); 437 for (AidGroup aidGroup : mStaticAidGroups.values()) { 438 out.append(aidGroup.toString()); 439 } 440 out.append(", Dynamic AID Groups: "); 441 for (AidGroup aidGroup : mDynamicAidGroups.values()) { 442 out.append(aidGroup.toString()); 443 } 444 return out.toString(); 445 } 446 447 @Override 448 public boolean equals(Object o) { 449 if (this == o) return true; 450 if (!(o instanceof ApduServiceInfo)) return false; 451 ApduServiceInfo thatService = (ApduServiceInfo) o; 452 453 return thatService.getComponent().equals(this.getComponent()); 454 } 455 456 @Override 457 public int hashCode() { 458 return getComponent().hashCode(); 459 } 460 461 462 @Override 463 public int describeContents() { 464 return 0; 465 } 466 467 @Override 468 public void writeToParcel(Parcel dest, int flags) { 469 mService.writeToParcel(dest, flags); 470 dest.writeString(mDescription); 471 dest.writeInt(mOnHost ? 1 : 0); 472 dest.writeInt(mStaticAidGroups.size()); 473 if (mStaticAidGroups.size() > 0) { 474 dest.writeTypedList(new ArrayList<AidGroup>(mStaticAidGroups.values())); 475 } 476 dest.writeInt(mDynamicAidGroups.size()); 477 if (mDynamicAidGroups.size() > 0) { 478 dest.writeTypedList(new ArrayList<AidGroup>(mDynamicAidGroups.values())); 479 } 480 dest.writeInt(mRequiresDeviceUnlock ? 1 : 0); 481 dest.writeInt(mBannerResourceId); 482 dest.writeInt(mUid); 483 dest.writeString(mSettingsActivityName); 484 }; 485 486 public static final Parcelable.Creator<ApduServiceInfo> CREATOR = 487 new Parcelable.Creator<ApduServiceInfo>() { 488 @Override 489 public ApduServiceInfo createFromParcel(Parcel source) { 490 ResolveInfo info = ResolveInfo.CREATOR.createFromParcel(source); 491 String description = source.readString(); 492 boolean onHost = source.readInt() != 0; 493 ArrayList<AidGroup> staticAidGroups = new ArrayList<AidGroup>(); 494 int numStaticGroups = source.readInt(); 495 if (numStaticGroups > 0) { 496 source.readTypedList(staticAidGroups, AidGroup.CREATOR); 497 } 498 ArrayList<AidGroup> dynamicAidGroups = new ArrayList<AidGroup>(); 499 int numDynamicGroups = source.readInt(); 500 if (numDynamicGroups > 0) { 501 source.readTypedList(dynamicAidGroups, AidGroup.CREATOR); 502 } 503 boolean requiresUnlock = source.readInt() != 0; 504 int bannerResource = source.readInt(); 505 int uid = source.readInt(); 506 String settingsActivityName = source.readString(); 507 return new ApduServiceInfo(info, onHost, description, staticAidGroups, 508 dynamicAidGroups, requiresUnlock, bannerResource, uid, 509 settingsActivityName); 510 } 511 512 @Override 513 public ApduServiceInfo[] newArray(int size) { 514 return new ApduServiceInfo[size]; 515 } 516 }; 517 518 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 519 pw.println(" " + getComponent() + 520 " (Description: " + getDescription() + ")"); 521 pw.println(" Static AID groups:"); 522 for (AidGroup group : mStaticAidGroups.values()) { 523 pw.println(" Category: " + group.category); 524 for (String aid : group.aids) { 525 pw.println(" AID: " + aid); 526 } 527 } 528 pw.println(" Dynamic AID groups:"); 529 for (AidGroup group : mDynamicAidGroups.values()) { 530 pw.println(" Category: " + group.category); 531 for (String aid : group.aids) { 532 pw.println(" AID: " + aid); 533 } 534 } 535 pw.println(" Settings Activity: " + mSettingsActivityName); 536 } 537 } 538