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