1 package com.android.nfc.cardemulation; 2 3 import android.app.ActivityManager; 4 import android.content.ComponentName; 5 import android.content.Context; 6 import android.content.Intent; 7 import android.database.ContentObserver; 8 import android.net.Uri; 9 import android.nfc.cardemulation.ApduServiceInfo; 10 import android.nfc.cardemulation.CardEmulation; 11 import android.nfc.cardemulation.ApduServiceInfo.AidGroup; 12 import android.os.Handler; 13 import android.os.Looper; 14 import android.os.UserHandle; 15 import android.provider.Settings; 16 import android.util.Log; 17 18 import com.google.android.collect.Maps; 19 20 import java.io.FileDescriptor; 21 import java.io.PrintWriter; 22 import java.util.ArrayList; 23 import java.util.HashMap; 24 import java.util.HashSet; 25 import java.util.List; 26 import java.util.Map; 27 import java.util.Set; 28 import java.util.SortedMap; 29 import java.util.TreeMap; 30 31 public class RegisteredAidCache implements RegisteredServicesCache.Callback { 32 static final String TAG = "RegisteredAidCache"; 33 34 static final boolean DBG = false; 35 36 // mAidServices is a tree that maps an AID to a list of handling services 37 // on Android. It is only valid for the current user. 38 final TreeMap<String, ArrayList<ApduServiceInfo>> mAidToServices = 39 new TreeMap<String, ArrayList<ApduServiceInfo>>(); 40 41 // mAidCache is a lookup table for quickly mapping an AID to one or 42 // more services. It differs from mAidServices in the sense that it 43 // has already accounted for defaults, and hence its return value 44 // is authoritative for the current set of services and defaults. 45 // It is only valid for the current user. 46 final HashMap<String, AidResolveInfo> mAidCache = 47 Maps.newHashMap(); 48 49 final HashMap<String, ComponentName> mCategoryDefaults = 50 Maps.newHashMap(); 51 52 final class AidResolveInfo { 53 List<ApduServiceInfo> services; 54 ApduServiceInfo defaultService; 55 String aid; 56 } 57 58 /** 59 * AIDs per category 60 */ 61 public final HashMap<String, Set<String>> mCategoryAids = 62 Maps.newHashMap(); 63 64 final Handler mHandler = new Handler(Looper.getMainLooper()); 65 final RegisteredServicesCache mServiceCache; 66 67 final Object mLock = new Object(); 68 final Context mContext; 69 final AidRoutingManager mRoutingManager; 70 final SettingsObserver mSettingsObserver; 71 72 ComponentName mNextTapComponent = null; 73 boolean mNfcEnabled = false; 74 75 private final class SettingsObserver extends ContentObserver { 76 public SettingsObserver(Handler handler) { 77 super(handler); 78 } 79 80 @Override 81 public void onChange(boolean selfChange, Uri uri) { 82 super.onChange(selfChange, uri); 83 synchronized (mLock) { 84 // Do it just for the current user. If it was in fact 85 // a change made for another user, we'll sync it down 86 // on user switch. 87 int currentUser = ActivityManager.getCurrentUser(); 88 boolean changed = updateFromSettingsLocked(currentUser); 89 if (changed) { 90 generateAidCacheLocked(); 91 updateRoutingLocked(); 92 } else { 93 if (DBG) Log.d(TAG, "Not updating aid cache + routing: nothing changed."); 94 } 95 } 96 } 97 }; 98 99 public RegisteredAidCache(Context context, AidRoutingManager routingManager) { 100 mSettingsObserver = new SettingsObserver(mHandler); 101 mContext = context; 102 mServiceCache = new RegisteredServicesCache(context, this); 103 mRoutingManager = routingManager; 104 105 mContext.getContentResolver().registerContentObserver( 106 Settings.Secure.getUriFor(Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT), 107 true, mSettingsObserver, UserHandle.USER_ALL); 108 updateFromSettingsLocked(ActivityManager.getCurrentUser()); 109 } 110 111 public boolean isNextTapOverriden() { 112 synchronized (mLock) { 113 return mNextTapComponent != null; 114 } 115 } 116 117 public AidResolveInfo resolveAidPrefix(String aid) { 118 synchronized (mLock) { 119 char nextAidChar = (char) (aid.charAt(aid.length() - 1) + 1); 120 String nextAid = aid.substring(0, aid.length() - 1) + nextAidChar; 121 SortedMap<String, ArrayList<ApduServiceInfo>> matches = 122 mAidToServices.subMap(aid, nextAid); 123 // The first match is lexicographically closest to what the reader asked; 124 if (matches.isEmpty()) { 125 return null; 126 } else { 127 AidResolveInfo resolveInfo = mAidCache.get(matches.firstKey()); 128 // Let the caller know which AID got selected 129 resolveInfo.aid = matches.firstKey(); 130 return resolveInfo; 131 } 132 } 133 } 134 135 public String getCategoryForAid(String aid) { 136 synchronized (mLock) { 137 Set<String> paymentAids = mCategoryAids.get(CardEmulation.CATEGORY_PAYMENT); 138 if (paymentAids != null && paymentAids.contains(aid)) { 139 return CardEmulation.CATEGORY_PAYMENT; 140 } else { 141 return CardEmulation.CATEGORY_OTHER; 142 } 143 } 144 } 145 146 public boolean isDefaultServiceForAid(int userId, ComponentName service, String aid) { 147 AidResolveInfo resolveInfo = null; 148 boolean serviceFound = false; 149 synchronized (mLock) { 150 serviceFound = mServiceCache.hasService(userId, service); 151 } 152 if (!serviceFound) { 153 // If we don't know about this service yet, it may have just been enabled 154 // using PackageManager.setComponentEnabledSetting(). The PackageManager 155 // broadcasts are delayed by 10 seconds in that scenario, which causes 156 // calls to our APIs referencing that service to fail. 157 // Hence, update the cache in case we don't know about the service. 158 if (DBG) Log.d(TAG, "Didn't find passed in service, invalidating cache."); 159 mServiceCache.invalidateCache(userId); 160 } 161 synchronized (mLock) { 162 resolveInfo = mAidCache.get(aid); 163 } 164 if (resolveInfo.services == null || resolveInfo.services.size() == 0) return false; 165 166 if (resolveInfo.defaultService != null) { 167 return service.equals(resolveInfo.defaultService.getComponent()); 168 } else if (resolveInfo.services.size() == 1) { 169 return service.equals(resolveInfo.services.get(0).getComponent()); 170 } else { 171 // More than one service, not the default 172 return false; 173 } 174 } 175 176 public boolean setDefaultServiceForCategory(int userId, ComponentName service, 177 String category) { 178 if (!CardEmulation.CATEGORY_PAYMENT.equals(category)) { 179 Log.e(TAG, "Not allowing defaults for category " + category); 180 return false; 181 } 182 synchronized (mLock) { 183 // TODO Not really nice to be writing to Settings.Secure here... 184 // ideally we overlay our local changes over whatever is in 185 // Settings.Secure 186 if (service == null || mServiceCache.hasService(userId, service)) { 187 Settings.Secure.putStringForUser(mContext.getContentResolver(), 188 Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, 189 service != null ? service.flattenToString() : null, userId); 190 } else { 191 Log.e(TAG, "Could not find default service to make default: " + service); 192 } 193 } 194 return true; 195 } 196 197 public boolean isDefaultServiceForCategory(int userId, String category, 198 ComponentName service) { 199 boolean serviceFound = false; 200 synchronized (mLock) { 201 // If we don't know about this service yet, it may have just been enabled 202 // using PackageManager.setComponentEnabledSetting(). The PackageManager 203 // broadcasts are delayed by 10 seconds in that scenario, which causes 204 // calls to our APIs referencing that service to fail. 205 // Hence, update the cache in case we don't know about the service. 206 serviceFound = mServiceCache.hasService(userId, service); 207 } 208 if (!serviceFound) { 209 if (DBG) Log.d(TAG, "Didn't find passed in service, invalidating cache."); 210 mServiceCache.invalidateCache(userId); 211 } 212 ComponentName defaultService = 213 getDefaultServiceForCategory(userId, category, true); 214 return (defaultService != null && defaultService.equals(service)); 215 } 216 217 ComponentName getDefaultServiceForCategory(int userId, String category, 218 boolean validateInstalled) { 219 if (!CardEmulation.CATEGORY_PAYMENT.equals(category)) { 220 Log.e(TAG, "Not allowing defaults for category " + category); 221 return null; 222 } 223 synchronized (mLock) { 224 // Load current payment default from settings 225 String name = Settings.Secure.getStringForUser( 226 mContext.getContentResolver(), Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, 227 userId); 228 if (name != null) { 229 ComponentName service = ComponentName.unflattenFromString(name); 230 if (!validateInstalled || service == null) { 231 return service; 232 } else { 233 return mServiceCache.hasService(userId, service) ? service : null; 234 } 235 } else { 236 return null; 237 } 238 } 239 } 240 241 public List<ApduServiceInfo> getServicesForCategory(int userId, String category) { 242 return mServiceCache.getServicesForCategory(userId, category); 243 } 244 245 public boolean setDefaultForNextTap(int userId, ComponentName service) { 246 synchronized (mLock) { 247 if (service != null) { 248 mNextTapComponent = service; 249 } else { 250 mNextTapComponent = null; 251 } 252 // Update cache and routing table 253 generateAidCacheLocked(); 254 updateRoutingLocked(); 255 } 256 return true; 257 } 258 259 /** 260 * Resolves an AID to a set of services that can handle it. 261 */ 262 AidResolveInfo resolveAidLocked(List<ApduServiceInfo> resolvedServices, String aid) { 263 if (resolvedServices == null || resolvedServices.size() == 0) { 264 if (DBG) Log.d(TAG, "Could not resolve AID " + aid + " to any service."); 265 return null; 266 } 267 AidResolveInfo resolveInfo = new AidResolveInfo(); 268 if (DBG) Log.d(TAG, "resolveAidLocked: resolving AID " + aid); 269 resolveInfo.services = new ArrayList<ApduServiceInfo>(); 270 resolveInfo.services.addAll(resolvedServices); 271 resolveInfo.defaultService = null; 272 273 ComponentName defaultComponent = mNextTapComponent; 274 if (DBG) Log.d(TAG, "resolveAidLocked: next tap component is " + defaultComponent); 275 Set<String> paymentAids = mCategoryAids.get(CardEmulation.CATEGORY_PAYMENT); 276 if (paymentAids != null && paymentAids.contains(aid)) { 277 if (DBG) Log.d(TAG, "resolveAidLocked: AID " + aid + " is a payment AID"); 278 // This AID has been registered as a payment AID by at least one service. 279 // Get default component for payment if no next tap default. 280 if (defaultComponent == null) { 281 defaultComponent = mCategoryDefaults.get(CardEmulation.CATEGORY_PAYMENT); 282 } 283 if (DBG) Log.d(TAG, "resolveAidLocked: default payment component is " 284 + defaultComponent); 285 if (resolvedServices.size() == 1) { 286 ApduServiceInfo resolvedService = resolvedServices.get(0); 287 if (DBG) Log.d(TAG, "resolveAidLocked: resolved single service " + 288 resolvedService.getComponent()); 289 if (defaultComponent != null && 290 defaultComponent.equals(resolvedService.getComponent())) { 291 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to (default) " + 292 resolvedService.getComponent()); 293 resolveInfo.defaultService = resolvedService; 294 } else { 295 // So..since we resolved to only one service, and this AID 296 // is a payment AID, we know that this service is the only 297 // service that has registered for this AID and in fact claimed 298 // it was a payment AID. 299 // There's two cases: 300 // 1. All other AIDs in the payment group are uncontended: 301 // in this case, just route to this app. It won't get 302 // in the way of other apps, and is likely to interact 303 // with different terminal infrastructure anyway. 304 // 2. At least one AID in the payment group is contended: 305 // in this case, we should ask the user to confirm, 306 // since it is likely to contend with other apps, even 307 // when touching the same terminal. 308 boolean foundConflict = false; 309 for (AidGroup aidGroup : resolvedService.getAidGroups()) { 310 if (aidGroup.getCategory().equals(CardEmulation.CATEGORY_PAYMENT)) { 311 for (String registeredAid : aidGroup.getAids()) { 312 ArrayList<ApduServiceInfo> servicesForAid = 313 mAidToServices.get(registeredAid); 314 if (servicesForAid != null && servicesForAid.size() > 1) { 315 foundConflict = true; 316 } 317 } 318 } 319 } 320 if (!foundConflict) { 321 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to " + 322 resolvedService.getComponent()); 323 // Treat this as if it's the default for this AID 324 resolveInfo.defaultService = resolvedService; 325 } else { 326 // Allow this service to handle, but don't set as default 327 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing AID " + aid + 328 " to " + resolvedService.getComponent() + 329 ", but will ask confirmation because its AID group is contended."); 330 } 331 } 332 } else if (resolvedServices.size() > 1) { 333 // More services have registered. If there's a default and it 334 // registered this AID, go with the default. Otherwise, add all. 335 if (DBG) Log.d(TAG, "resolveAidLocked: multiple services matched."); 336 if (defaultComponent != null) { 337 for (ApduServiceInfo service : resolvedServices) { 338 if (service.getComponent().equals(defaultComponent)) { 339 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to (default) " 340 + service.getComponent()); 341 resolveInfo.defaultService = service; 342 break; 343 } 344 } 345 if (resolveInfo.defaultService == null) { 346 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to all services"); 347 } 348 } 349 } // else -> should not hit, we checked for 0 before. 350 } else { 351 // This AID is not a payment AID, just return all components 352 // that can handle it, but be mindful of (next tap) defaults. 353 for (ApduServiceInfo service : resolvedServices) { 354 if (service.getComponent().equals(defaultComponent)) { 355 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, " + 356 "routing to (default) " + service.getComponent()); 357 resolveInfo.defaultService = service; 358 break; 359 } 360 } 361 if (resolveInfo.defaultService == null) { 362 // If we didn't find the default, mark the first as default 363 // if there is only one. 364 if (resolveInfo.services.size() == 1) { 365 resolveInfo.defaultService = resolveInfo.services.get(0); 366 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, " + 367 "routing to (default) " + resolveInfo.defaultService.getComponent()); 368 } else { 369 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, routing all"); 370 } 371 } 372 } 373 return resolveInfo; 374 } 375 376 void generateAidTreeLocked(List<ApduServiceInfo> services) { 377 // Easiest is to just build the entire tree again 378 mAidToServices.clear(); 379 for (ApduServiceInfo service : services) { 380 if (DBG) Log.d(TAG, "generateAidTree component: " + service.getComponent()); 381 for (String aid : service.getAids()) { 382 if (DBG) Log.d(TAG, "generateAidTree AID: " + aid); 383 // Check if a mapping exists for this AID 384 if (mAidToServices.containsKey(aid)) { 385 final ArrayList<ApduServiceInfo> aidServices = mAidToServices.get(aid); 386 aidServices.add(service); 387 } else { 388 final ArrayList<ApduServiceInfo> aidServices = 389 new ArrayList<ApduServiceInfo>(); 390 aidServices.add(service); 391 mAidToServices.put(aid, aidServices); 392 } 393 } 394 } 395 } 396 397 void generateAidCategoriesLocked(List<ApduServiceInfo> services) { 398 // Trash existing mapping 399 mCategoryAids.clear(); 400 401 for (ApduServiceInfo service : services) { 402 ArrayList<AidGroup> aidGroups = service.getAidGroups(); 403 if (aidGroups == null) continue; 404 for (AidGroup aidGroup : aidGroups) { 405 String groupCategory = aidGroup.getCategory(); 406 Set<String> categoryAids = mCategoryAids.get(groupCategory); 407 if (categoryAids == null) { 408 categoryAids = new HashSet<String>(); 409 } 410 categoryAids.addAll(aidGroup.getAids()); 411 mCategoryAids.put(groupCategory, categoryAids); 412 } 413 } 414 } 415 416 boolean updateFromSettingsLocked(int userId) { 417 // Load current payment default from settings 418 String name = Settings.Secure.getStringForUser( 419 mContext.getContentResolver(), Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, 420 userId); 421 ComponentName newDefault = name != null ? ComponentName.unflattenFromString(name) : null; 422 ComponentName oldDefault = mCategoryDefaults.put(CardEmulation.CATEGORY_PAYMENT, 423 newDefault); 424 if (DBG) Log.d(TAG, "Updating default component to: " + (name != null ? 425 ComponentName.unflattenFromString(name) : "null")); 426 return newDefault != oldDefault; 427 } 428 429 void generateAidCacheLocked() { 430 mAidCache.clear(); 431 for (Map.Entry<String, ArrayList<ApduServiceInfo>> aidEntry: 432 mAidToServices.entrySet()) { 433 String aid = aidEntry.getKey(); 434 if (!mAidCache.containsKey(aid)) { 435 mAidCache.put(aid, resolveAidLocked(aidEntry.getValue(), aid)); 436 } 437 } 438 } 439 440 void updateRoutingLocked() { 441 if (!mNfcEnabled) { 442 if (DBG) Log.d(TAG, "Not updating routing table because NFC is off."); 443 return; 444 } 445 final Set<String> handledAids = new HashSet<String>(); 446 // For each AID, find interested services 447 for (Map.Entry<String, AidResolveInfo> aidEntry: 448 mAidCache.entrySet()) { 449 String aid = aidEntry.getKey(); 450 AidResolveInfo resolveInfo = aidEntry.getValue(); 451 if (resolveInfo.services.size() == 0) { 452 // No interested services, if there is a current routing remove it 453 mRoutingManager.removeAid(aid); 454 } else if (resolveInfo.defaultService != null) { 455 // There is a default service set, route to that service 456 mRoutingManager.setRouteForAid(aid, resolveInfo.defaultService.isOnHost()); 457 } else if (resolveInfo.services.size() == 1) { 458 // Only one service, but not the default, must route to host 459 // to ask the user to confirm. 460 mRoutingManager.setRouteForAid(aid, true); 461 } else if (resolveInfo.services.size() > 1) { 462 // Multiple services, need to route to host to ask 463 mRoutingManager.setRouteForAid(aid, true); 464 } 465 handledAids.add(aid); 466 } 467 // Now, find AIDs in the routing table that are no longer routed to 468 // and remove them. 469 Set<String> routedAids = mRoutingManager.getRoutedAids(); 470 for (String aid : routedAids) { 471 if (!handledAids.contains(aid)) { 472 if (DBG) Log.d(TAG, "Removing routing for AID " + aid + ", because " + 473 "there are no no interested services."); 474 mRoutingManager.removeAid(aid); 475 } 476 } 477 // And commit the routing 478 mRoutingManager.commitRouting(); 479 } 480 481 void showDefaultRemovedDialog() { 482 Intent intent = new Intent(mContext, DefaultRemovedActivity.class); 483 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 484 mContext.startActivityAsUser(intent, UserHandle.CURRENT); 485 } 486 487 void onPaymentDefaultRemoved(int userId, List<ApduServiceInfo> services) { 488 int numPaymentServices = 0; 489 ComponentName lastFoundPaymentService = null; 490 for (ApduServiceInfo service : services) { 491 if (service.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { 492 numPaymentServices++; 493 lastFoundPaymentService = service.getComponent(); 494 } 495 } 496 if (DBG) Log.d(TAG, "Number of payment services is " + 497 Integer.toString(numPaymentServices)); 498 if (numPaymentServices == 0) { 499 if (DBG) Log.d(TAG, "Default removed, no services left."); 500 // No payment services left, unset default and don't ask the user 501 setDefaultServiceForCategory(userId, null, 502 CardEmulation.CATEGORY_PAYMENT); 503 } else if (numPaymentServices == 1) { 504 // Only one left, automatically make it the default 505 if (DBG) Log.d(TAG, "Default removed, making remaining service default."); 506 setDefaultServiceForCategory(userId, lastFoundPaymentService, 507 CardEmulation.CATEGORY_PAYMENT); 508 } else if (numPaymentServices > 1) { 509 // More than one left, unset default and ask the user if he wants 510 // to set a new one 511 if (DBG) Log.d(TAG, "Default removed, asking user to pick."); 512 setDefaultServiceForCategory(userId, null, 513 CardEmulation.CATEGORY_PAYMENT); 514 showDefaultRemovedDialog(); 515 } 516 } 517 518 void setDefaultIfNeededLocked(int userId, List<ApduServiceInfo> services) { 519 int numPaymentServices = 0; 520 ComponentName lastFoundPaymentService = null; 521 for (ApduServiceInfo service : services) { 522 if (service.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { 523 numPaymentServices++; 524 lastFoundPaymentService = service.getComponent(); 525 } 526 } 527 if (numPaymentServices > 1) { 528 // More than one service left, leave default unset 529 if (DBG) Log.d(TAG, "No default set, more than one service left."); 530 } else if (numPaymentServices == 1) { 531 // Make single found payment service the default 532 if (DBG) Log.d(TAG, "No default set, making single service default."); 533 setDefaultServiceForCategory(userId, lastFoundPaymentService, 534 CardEmulation.CATEGORY_PAYMENT); 535 } else { 536 // No payment services left, leave default at null 537 if (DBG) Log.d(TAG, "No default set, last payment service removed."); 538 } 539 } 540 541 void checkDefaultsLocked(int userId, List<ApduServiceInfo> services) { 542 ComponentName defaultPaymentService = 543 getDefaultServiceForCategory(userId, CardEmulation.CATEGORY_PAYMENT, false); 544 if (DBG) Log.d(TAG, "Current default: " + defaultPaymentService); 545 if (defaultPaymentService != null) { 546 // Validate the default is still installed and handling payment 547 ApduServiceInfo serviceInfo = mServiceCache.getService(userId, defaultPaymentService); 548 if (serviceInfo == null) { 549 Log.e(TAG, "Default payment service unexpectedly removed."); 550 onPaymentDefaultRemoved(userId, services); 551 } else if (!serviceInfo.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { 552 if (DBG) Log.d(TAG, "Default payment service had payment category removed"); 553 onPaymentDefaultRemoved(userId, services); 554 } else { 555 // Default still exists and handles the category, nothing do 556 if (DBG) Log.d(TAG, "Default payment service still ok."); 557 } 558 } else { 559 // A payment service may have been removed, leaving only one; 560 // in that case, automatically set that app as default. 561 setDefaultIfNeededLocked(userId, services); 562 } 563 updateFromSettingsLocked(userId); 564 } 565 566 @Override 567 public void onServicesUpdated(int userId, List<ApduServiceInfo> services) { 568 synchronized (mLock) { 569 if (ActivityManager.getCurrentUser() == userId) { 570 // Rebuild our internal data-structures 571 checkDefaultsLocked(userId, services); 572 generateAidTreeLocked(services); 573 generateAidCategoriesLocked(services); 574 generateAidCacheLocked(); 575 updateRoutingLocked(); 576 } else { 577 if (DBG) Log.d(TAG, "Ignoring update because it's not for the current user."); 578 } 579 } 580 } 581 582 public void invalidateCache(int currentUser) { 583 mServiceCache.invalidateCache(currentUser); 584 } 585 586 public void onNfcDisabled() { 587 synchronized (mLock) { 588 mNfcEnabled = false; 589 } 590 mServiceCache.onNfcDisabled(); 591 mRoutingManager.onNfccRoutingTableCleared(); 592 } 593 594 public void onNfcEnabled() { 595 synchronized (mLock) { 596 mNfcEnabled = true; 597 updateFromSettingsLocked(ActivityManager.getCurrentUser()); 598 } 599 mServiceCache.onNfcEnabled(); 600 } 601 602 String dumpEntry(Map.Entry<String, AidResolveInfo> entry) { 603 StringBuilder sb = new StringBuilder(); 604 sb.append(" \"" + entry.getKey() + "\"\n"); 605 ApduServiceInfo defaultService = entry.getValue().defaultService; 606 ComponentName defaultComponent = defaultService != null ? 607 defaultService.getComponent() : null; 608 609 for (ApduServiceInfo service : entry.getValue().services) { 610 sb.append(" "); 611 if (service.getComponent().equals(defaultComponent)) { 612 sb.append("*DEFAULT* "); 613 } 614 sb.append(service.getComponent() + 615 " (Description: " + service.getDescription() + ")\n"); 616 } 617 return sb.toString(); 618 } 619 620 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 621 mServiceCache.dump(fd, pw, args); 622 pw.println("AID cache entries: "); 623 for (Map.Entry<String, AidResolveInfo> entry : mAidCache.entrySet()) { 624 pw.println(dumpEntry(entry)); 625 } 626 pw.println("Category defaults: "); 627 for (Map.Entry<String, ComponentName> entry : mCategoryDefaults.entrySet()) { 628 pw.println(" " + entry.getKey() + "->" + entry.getValue()); 629 } 630 pw.println(""); 631 } 632 } 633