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 } 257 } 258 } catch (NameNotFoundException e) { 259 throw new XmlPullParserException("Unable to create context for: " + si.packageName); 260 } finally { 261 if (parser != null) parser.close(); 262 } 263 // Set uid 264 mUid = si.applicationInfo.uid; 265 } 266 267 public ComponentName getComponent() { 268 return new ComponentName(mService.serviceInfo.packageName, 269 mService.serviceInfo.name); 270 } 271 272 /** 273 * Returns a consolidated list of AIDs from the AID groups 274 * registered by this service. Note that if a service has both 275 * a static (manifest-based) AID group for a category and a dynamic 276 * AID group, only the dynamically registered AIDs will be returned 277 * for that category. 278 * @return List of AIDs registered by the service 279 */ 280 public List<String> getAids() { 281 final ArrayList<String> aids = new ArrayList<String>(); 282 for (AidGroup group : getAidGroups()) { 283 aids.addAll(group.aids); 284 } 285 return aids; 286 } 287 288 public List<String> getPrefixAids() { 289 final ArrayList<String> prefixAids = new ArrayList<String>(); 290 for (AidGroup group : getAidGroups()) { 291 for (String aid : group.aids) { 292 if (aid.endsWith("*")) { 293 prefixAids.add(aid); 294 } 295 } 296 } 297 return prefixAids; 298 } 299 300 /** 301 * Returns the registered AID group for this category. 302 */ 303 public AidGroup getDynamicAidGroupForCategory(String category) { 304 return mDynamicAidGroups.get(category); 305 } 306 307 public boolean removeDynamicAidGroupForCategory(String category) { 308 return (mDynamicAidGroups.remove(category) != null); 309 } 310 311 /** 312 * Returns a consolidated list of AID groups 313 * registered by this service. Note that if a service has both 314 * a static (manifest-based) AID group for a category and a dynamic 315 * AID group, only the dynamically registered AID group will be returned 316 * for that category. 317 * @return List of AIDs registered by the service 318 */ 319 public ArrayList<AidGroup> getAidGroups() { 320 final ArrayList<AidGroup> groups = new ArrayList<AidGroup>(); 321 for (Map.Entry<String, AidGroup> entry : mDynamicAidGroups.entrySet()) { 322 groups.add(entry.getValue()); 323 } 324 for (Map.Entry<String, AidGroup> entry : mStaticAidGroups.entrySet()) { 325 if (!mDynamicAidGroups.containsKey(entry.getKey())) { 326 // Consolidate AID groups - don't return static ones 327 // if a dynamic group exists for the category. 328 groups.add(entry.getValue()); 329 } 330 } 331 return groups; 332 } 333 334 /** 335 * Returns the category to which this service has attributed the AID that is passed in, 336 * or null if we don't know this AID. 337 */ 338 public String getCategoryForAid(String aid) { 339 ArrayList<AidGroup> groups = getAidGroups(); 340 for (AidGroup group : groups) { 341 if (group.aids.contains(aid.toUpperCase())) { 342 return group.category; 343 } 344 } 345 return null; 346 } 347 348 public boolean hasCategory(String category) { 349 return (mStaticAidGroups.containsKey(category) || mDynamicAidGroups.containsKey(category)); 350 } 351 352 public boolean isOnHost() { 353 return mOnHost; 354 } 355 356 public boolean requiresUnlock() { 357 return mRequiresDeviceUnlock; 358 } 359 360 public String getDescription() { 361 return mDescription; 362 } 363 364 public int getUid() { 365 return mUid; 366 } 367 368 public void setOrReplaceDynamicAidGroup(AidGroup aidGroup) { 369 mDynamicAidGroups.put(aidGroup.getCategory(), aidGroup); 370 } 371 372 public CharSequence loadLabel(PackageManager pm) { 373 return mService.loadLabel(pm); 374 } 375 376 public CharSequence loadAppLabel(PackageManager pm) { 377 try { 378 return pm.getApplicationLabel(pm.getApplicationInfo( 379 mService.resolvePackageName, PackageManager.GET_META_DATA)); 380 } catch (PackageManager.NameNotFoundException e) { 381 return null; 382 } 383 } 384 385 public Drawable loadIcon(PackageManager pm) { 386 return mService.loadIcon(pm); 387 } 388 389 public Drawable loadBanner(PackageManager pm) { 390 Resources res; 391 try { 392 res = pm.getResourcesForApplication(mService.serviceInfo.packageName); 393 Drawable banner = res.getDrawable(mBannerResourceId); 394 return banner; 395 } catch (NotFoundException e) { 396 Log.e(TAG, "Could not load banner."); 397 return null; 398 } catch (NameNotFoundException e) { 399 Log.e(TAG, "Could not load banner."); 400 return null; 401 } 402 } 403 404 public String getSettingsActivityName() { return mSettingsActivityName; } 405 406 @Override 407 public String toString() { 408 StringBuilder out = new StringBuilder("ApduService: "); 409 out.append(getComponent()); 410 out.append(", description: " + mDescription); 411 out.append(", Static AID Groups: "); 412 for (AidGroup aidGroup : mStaticAidGroups.values()) { 413 out.append(aidGroup.toString()); 414 } 415 out.append(", Dynamic AID Groups: "); 416 for (AidGroup aidGroup : mDynamicAidGroups.values()) { 417 out.append(aidGroup.toString()); 418 } 419 return out.toString(); 420 } 421 422 @Override 423 public boolean equals(Object o) { 424 if (this == o) return true; 425 if (!(o instanceof ApduServiceInfo)) return false; 426 ApduServiceInfo thatService = (ApduServiceInfo) o; 427 428 return thatService.getComponent().equals(this.getComponent()); 429 } 430 431 @Override 432 public int hashCode() { 433 return getComponent().hashCode(); 434 } 435 436 437 @Override 438 public int describeContents() { 439 return 0; 440 } 441 442 @Override 443 public void writeToParcel(Parcel dest, int flags) { 444 mService.writeToParcel(dest, flags); 445 dest.writeString(mDescription); 446 dest.writeInt(mOnHost ? 1 : 0); 447 dest.writeInt(mStaticAidGroups.size()); 448 if (mStaticAidGroups.size() > 0) { 449 dest.writeTypedList(new ArrayList<AidGroup>(mStaticAidGroups.values())); 450 } 451 dest.writeInt(mDynamicAidGroups.size()); 452 if (mDynamicAidGroups.size() > 0) { 453 dest.writeTypedList(new ArrayList<AidGroup>(mDynamicAidGroups.values())); 454 } 455 dest.writeInt(mRequiresDeviceUnlock ? 1 : 0); 456 dest.writeInt(mBannerResourceId); 457 dest.writeInt(mUid); 458 dest.writeString(mSettingsActivityName); 459 }; 460 461 public static final Parcelable.Creator<ApduServiceInfo> CREATOR = 462 new Parcelable.Creator<ApduServiceInfo>() { 463 @Override 464 public ApduServiceInfo createFromParcel(Parcel source) { 465 ResolveInfo info = ResolveInfo.CREATOR.createFromParcel(source); 466 String description = source.readString(); 467 boolean onHost = source.readInt() != 0; 468 ArrayList<AidGroup> staticAidGroups = new ArrayList<AidGroup>(); 469 int numStaticGroups = source.readInt(); 470 if (numStaticGroups > 0) { 471 source.readTypedList(staticAidGroups, AidGroup.CREATOR); 472 } 473 ArrayList<AidGroup> dynamicAidGroups = new ArrayList<AidGroup>(); 474 int numDynamicGroups = source.readInt(); 475 if (numDynamicGroups > 0) { 476 source.readTypedList(dynamicAidGroups, AidGroup.CREATOR); 477 } 478 boolean requiresUnlock = source.readInt() != 0; 479 int bannerResource = source.readInt(); 480 int uid = source.readInt(); 481 String settingsActivityName = source.readString(); 482 return new ApduServiceInfo(info, onHost, description, staticAidGroups, 483 dynamicAidGroups, requiresUnlock, bannerResource, uid, 484 settingsActivityName); 485 } 486 487 @Override 488 public ApduServiceInfo[] newArray(int size) { 489 return new ApduServiceInfo[size]; 490 } 491 }; 492 493 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 494 pw.println(" " + getComponent() + 495 " (Description: " + getDescription() + ")"); 496 pw.println(" Static AID groups:"); 497 for (AidGroup group : mStaticAidGroups.values()) { 498 pw.println(" Category: " + group.category); 499 for (String aid : group.aids) { 500 pw.println(" AID: " + aid); 501 } 502 } 503 pw.println(" Dynamic AID groups:"); 504 for (AidGroup group : mDynamicAidGroups.values()) { 505 pw.println(" Category: " + group.category); 506 for (String aid : group.aids) { 507 pw.println(" AID: " + aid); 508 } 509 } 510 pw.println(" Settings Activity: " + mSettingsActivityName); 511 } 512 } 513