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