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 == null || resolveInfo.services == null || 165 resolveInfo.services.size() == 0) { 166 return false; 167 } 168 169 if (resolveInfo.defaultService != null) { 170 return service.equals(resolveInfo.defaultService.getComponent()); 171 } else if (resolveInfo.services.size() == 1) { 172 return service.equals(resolveInfo.services.get(0).getComponent()); 173 } else { 174 // More than one service, not the default 175 return false; 176 } 177 } 178 179 public boolean setDefaultServiceForCategory(int userId, ComponentName service, 180 String category) { 181 if (!CardEmulation.CATEGORY_PAYMENT.equals(category)) { 182 Log.e(TAG, "Not allowing defaults for category " + category); 183 return false; 184 } 185 synchronized (mLock) { 186 // TODO Not really nice to be writing to Settings.Secure here... 187 // ideally we overlay our local changes over whatever is in 188 // Settings.Secure 189 if (service == null || mServiceCache.hasService(userId, service)) { 190 Settings.Secure.putStringForUser(mContext.getContentResolver(), 191 Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, 192 service != null ? service.flattenToString() : null, userId); 193 } else { 194 Log.e(TAG, "Could not find default service to make default: " + service); 195 } 196 } 197 return true; 198 } 199 200 public boolean isDefaultServiceForCategory(int userId, String category, 201 ComponentName service) { 202 boolean serviceFound = false; 203 synchronized (mLock) { 204 // If we don't know about this service yet, it may have just been enabled 205 // using PackageManager.setComponentEnabledSetting(). The PackageManager 206 // broadcasts are delayed by 10 seconds in that scenario, which causes 207 // calls to our APIs referencing that service to fail. 208 // Hence, update the cache in case we don't know about the service. 209 serviceFound = mServiceCache.hasService(userId, service); 210 } 211 if (!serviceFound) { 212 if (DBG) Log.d(TAG, "Didn't find passed in service, invalidating cache."); 213 mServiceCache.invalidateCache(userId); 214 } 215 ComponentName defaultService = 216 getDefaultServiceForCategory(userId, category, true); 217 return (defaultService != null && defaultService.equals(service)); 218 } 219 220 ComponentName getDefaultServiceForCategory(int userId, String category, 221 boolean validateInstalled) { 222 if (!CardEmulation.CATEGORY_PAYMENT.equals(category)) { 223 Log.e(TAG, "Not allowing defaults for category " + category); 224 return null; 225 } 226 synchronized (mLock) { 227 // Load current payment default from settings 228 String name = Settings.Secure.getStringForUser( 229 mContext.getContentResolver(), Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, 230 userId); 231 if (name != null) { 232 ComponentName service = ComponentName.unflattenFromString(name); 233 if (!validateInstalled || service == null) { 234 return service; 235 } else { 236 return mServiceCache.hasService(userId, service) ? service : null; 237 } 238 } else { 239 return null; 240 } 241 } 242 } 243 244 public List<ApduServiceInfo> getServicesForCategory(int userId, String category) { 245 return mServiceCache.getServicesForCategory(userId, category); 246 } 247 248 public boolean setDefaultForNextTap(int userId, ComponentName service) { 249 synchronized (mLock) { 250 if (service != null) { 251 mNextTapComponent = service; 252 } else { 253 mNextTapComponent = null; 254 } 255 // Update cache and routing table 256 generateAidCacheLocked(); 257 updateRoutingLocked(); 258 } 259 return true; 260 } 261 262 /** 263 * Resolves an AID to a set of services that can handle it. 264 */ 265 AidResolveInfo resolveAidLocked(List<ApduServiceInfo> resolvedServices, String aid) { 266 if (resolvedServices == null || resolvedServices.size() == 0) { 267 if (DBG) Log.d(TAG, "Could not resolve AID " + aid + " to any service."); 268 return null; 269 } 270 AidResolveInfo resolveInfo = new AidResolveInfo(); 271 if (DBG) Log.d(TAG, "resolveAidLocked: resolving AID " + aid); 272 resolveInfo.services = new ArrayList<ApduServiceInfo>(); 273 resolveInfo.services.addAll(resolvedServices); 274 resolveInfo.defaultService = null; 275 276 ComponentName defaultComponent = mNextTapComponent; 277 if (DBG) Log.d(TAG, "resolveAidLocked: next tap component is " + defaultComponent); 278 Set<String> paymentAids = mCategoryAids.get(CardEmulation.CATEGORY_PAYMENT); 279 if (paymentAids != null && paymentAids.contains(aid)) { 280 if (DBG) Log.d(TAG, "resolveAidLocked: AID " + aid + " is a payment AID"); 281 // This AID has been registered as a payment AID by at least one service. 282 // Get default component for payment if no next tap default. 283 if (defaultComponent == null) { 284 defaultComponent = mCategoryDefaults.get(CardEmulation.CATEGORY_PAYMENT); 285 } 286 if (DBG) Log.d(TAG, "resolveAidLocked: default payment component is " 287 + defaultComponent); 288 if (resolvedServices.size() == 1) { 289 ApduServiceInfo resolvedService = resolvedServices.get(0); 290 if (DBG) Log.d(TAG, "resolveAidLocked: resolved single service " + 291 resolvedService.getComponent()); 292 if (defaultComponent != null && 293 defaultComponent.equals(resolvedService.getComponent())) { 294 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to (default) " + 295 resolvedService.getComponent()); 296 resolveInfo.defaultService = resolvedService; 297 } else { 298 // So..since we resolved to only one service, and this AID 299 // is a payment AID, we know that this service is the only 300 // service that has registered for this AID and in fact claimed 301 // it was a payment AID. 302 // There's two cases: 303 // 1. All other AIDs in the payment group are uncontended: 304 // in this case, just route to this app. It won't get 305 // in the way of other apps, and is likely to interact 306 // with different terminal infrastructure anyway. 307 // 2. At least one AID in the payment group is contended: 308 // in this case, we should ask the user to confirm, 309 // since it is likely to contend with other apps, even 310 // when touching the same terminal. 311 boolean foundConflict = false; 312 for (AidGroup aidGroup : resolvedService.getAidGroups()) { 313 if (aidGroup.getCategory().equals(CardEmulation.CATEGORY_PAYMENT)) { 314 for (String registeredAid : aidGroup.getAids()) { 315 ArrayList<ApduServiceInfo> servicesForAid = 316 mAidToServices.get(registeredAid); 317 if (servicesForAid != null && servicesForAid.size() > 1) { 318 foundConflict = true; 319 } 320 } 321 } 322 } 323 if (!foundConflict) { 324 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to " + 325 resolvedService.getComponent()); 326 // Treat this as if it's the default for this AID 327 resolveInfo.defaultService = resolvedService; 328 } else { 329 // Allow this service to handle, but don't set as default 330 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing AID " + aid + 331 " to " + resolvedService.getComponent() + 332 ", but will ask confirmation because its AID group is contended."); 333 } 334 } 335 } else if (resolvedServices.size() > 1) { 336 // More services have registered. If there's a default and it 337 // registered this AID, go with the default. Otherwise, add all. 338 if (DBG) Log.d(TAG, "resolveAidLocked: multiple services matched."); 339 if (defaultComponent != null) { 340 for (ApduServiceInfo service : resolvedServices) { 341 if (service.getComponent().equals(defaultComponent)) { 342 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to (default) " 343 + service.getComponent()); 344 resolveInfo.defaultService = service; 345 break; 346 } 347 } 348 if (resolveInfo.defaultService == null) { 349 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to all services"); 350 } 351 } 352 } // else -> should not hit, we checked for 0 before. 353 } else { 354 // This AID is not a payment AID, just return all components 355 // that can handle it, but be mindful of (next tap) defaults. 356 for (ApduServiceInfo service : resolvedServices) { 357 if (service.getComponent().equals(defaultComponent)) { 358 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, " + 359 "routing to (default) " + service.getComponent()); 360 resolveInfo.defaultService = service; 361 break; 362 } 363 } 364 if (resolveInfo.defaultService == null) { 365 // If we didn't find the default, mark the first as default 366 // if there is only one. 367 if (resolveInfo.services.size() == 1) { 368 resolveInfo.defaultService = resolveInfo.services.get(0); 369 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, " + 370 "routing to (default) " + resolveInfo.defaultService.getComponent()); 371 } else { 372 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, routing all"); 373 } 374 } 375 } 376 return resolveInfo; 377 } 378 379 void generateAidTreeLocked(List<ApduServiceInfo> services) { 380 // Easiest is to just build the entire tree again 381 mAidToServices.clear(); 382 for (ApduServiceInfo service : services) { 383 if (DBG) Log.d(TAG, "generateAidTree component: " + service.getComponent()); 384 for (String aid : service.getAids()) { 385 if (DBG) Log.d(TAG, "generateAidTree AID: " + aid); 386 // Check if a mapping exists for this AID 387 if (mAidToServices.containsKey(aid)) { 388 final ArrayList<ApduServiceInfo> aidServices = mAidToServices.get(aid); 389 aidServices.add(service); 390 } else { 391 final ArrayList<ApduServiceInfo> aidServices = 392 new ArrayList<ApduServiceInfo>(); 393 aidServices.add(service); 394 mAidToServices.put(aid, aidServices); 395 } 396 } 397 } 398 } 399 400 void generateAidCategoriesLocked(List<ApduServiceInfo> services) { 401 // Trash existing mapping 402 mCategoryAids.clear(); 403 404 for (ApduServiceInfo service : services) { 405 ArrayList<AidGroup> aidGroups = service.getAidGroups(); 406 if (aidGroups == null) continue; 407 for (AidGroup aidGroup : aidGroups) { 408 String groupCategory = aidGroup.getCategory(); 409 Set<String> categoryAids = mCategoryAids.get(groupCategory); 410 if (categoryAids == null) { 411 categoryAids = new HashSet<String>(); 412 } 413 categoryAids.addAll(aidGroup.getAids()); 414 mCategoryAids.put(groupCategory, categoryAids); 415 } 416 } 417 } 418 419 boolean updateFromSettingsLocked(int userId) { 420 // Load current payment default from settings 421 String name = Settings.Secure.getStringForUser( 422 mContext.getContentResolver(), Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, 423 userId); 424 ComponentName newDefault = name != null ? ComponentName.unflattenFromString(name) : null; 425 ComponentName oldDefault = mCategoryDefaults.put(CardEmulation.CATEGORY_PAYMENT, 426 newDefault); 427 if (DBG) Log.d(TAG, "Updating default component to: " + (name != null ? 428 ComponentName.unflattenFromString(name) : "null")); 429 return newDefault != oldDefault; 430 } 431 432 void generateAidCacheLocked() { 433 mAidCache.clear(); 434 for (Map.Entry<String, ArrayList<ApduServiceInfo>> aidEntry: 435 mAidToServices.entrySet()) { 436 String aid = aidEntry.getKey(); 437 if (!mAidCache.containsKey(aid)) { 438 mAidCache.put(aid, resolveAidLocked(aidEntry.getValue(), aid)); 439 } 440 } 441 } 442 443 void updateRoutingLocked() { 444 if (!mNfcEnabled) { 445 if (DBG) Log.d(TAG, "Not updating routing table because NFC is off."); 446 return; 447 } 448 final Set<String> handledAids = new HashSet<String>(); 449 // For each AID, find interested services 450 for (Map.Entry<String, AidResolveInfo> aidEntry: 451 mAidCache.entrySet()) { 452 String aid = aidEntry.getKey(); 453 AidResolveInfo resolveInfo = aidEntry.getValue(); 454 if (resolveInfo.services.size() == 0) { 455 // No interested services, if there is a current routing remove it 456 mRoutingManager.removeAid(aid); 457 } else if (resolveInfo.defaultService != null) { 458 // There is a default service set, route to that service 459 mRoutingManager.setRouteForAid(aid, resolveInfo.defaultService.isOnHost()); 460 } else if (resolveInfo.services.size() == 1) { 461 // Only one service, but not the default, must route to host 462 // to ask the user to confirm. 463 mRoutingManager.setRouteForAid(aid, true); 464 } else if (resolveInfo.services.size() > 1) { 465 // Multiple services, need to route to host to ask 466 mRoutingManager.setRouteForAid(aid, true); 467 } 468 handledAids.add(aid); 469 } 470 // Now, find AIDs in the routing table that are no longer routed to 471 // and remove them. 472 Set<String> routedAids = mRoutingManager.getRoutedAids(); 473 for (String aid : routedAids) { 474 if (!handledAids.contains(aid)) { 475 if (DBG) Log.d(TAG, "Removing routing for AID " + aid + ", because " + 476 "there are no no interested services."); 477 mRoutingManager.removeAid(aid); 478 } 479 } 480 // And commit the routing 481 mRoutingManager.commitRouting(); 482 } 483 484 void showDefaultRemovedDialog() { 485 Intent intent = new Intent(mContext, DefaultRemovedActivity.class); 486 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 487 mContext.startActivityAsUser(intent, UserHandle.CURRENT); 488 } 489 490 void onPaymentDefaultRemoved(int userId, List<ApduServiceInfo> services) { 491 int numPaymentServices = 0; 492 ComponentName lastFoundPaymentService = null; 493 for (ApduServiceInfo service : services) { 494 if (service.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { 495 numPaymentServices++; 496 lastFoundPaymentService = service.getComponent(); 497 } 498 } 499 if (DBG) Log.d(TAG, "Number of payment services is " + 500 Integer.toString(numPaymentServices)); 501 if (numPaymentServices == 0) { 502 if (DBG) Log.d(TAG, "Default removed, no services left."); 503 // No payment services left, unset default and don't ask the user 504 setDefaultServiceForCategory(userId, null, 505 CardEmulation.CATEGORY_PAYMENT); 506 } else if (numPaymentServices == 1) { 507 // Only one left, automatically make it the default 508 if (DBG) Log.d(TAG, "Default removed, making remaining service default."); 509 setDefaultServiceForCategory(userId, lastFoundPaymentService, 510 CardEmulation.CATEGORY_PAYMENT); 511 } else if (numPaymentServices > 1) { 512 // More than one left, unset default and ask the user if he wants 513 // to set a new one 514 if (DBG) Log.d(TAG, "Default removed, asking user to pick."); 515 setDefaultServiceForCategory(userId, null, 516 CardEmulation.CATEGORY_PAYMENT); 517 showDefaultRemovedDialog(); 518 } 519 } 520 521 void setDefaultIfNeededLocked(int userId, List<ApduServiceInfo> services) { 522 int numPaymentServices = 0; 523 ComponentName lastFoundPaymentService = null; 524 for (ApduServiceInfo service : services) { 525 if (service.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { 526 numPaymentServices++; 527 lastFoundPaymentService = service.getComponent(); 528 } 529 } 530 if (numPaymentServices > 1) { 531 // More than one service left, leave default unset 532 if (DBG) Log.d(TAG, "No default set, more than one service left."); 533 } else if (numPaymentServices == 1) { 534 // Make single found payment service the default 535 if (DBG) Log.d(TAG, "No default set, making single service default."); 536 setDefaultServiceForCategory(userId, lastFoundPaymentService, 537 CardEmulation.CATEGORY_PAYMENT); 538 } else { 539 // No payment services left, leave default at null 540 if (DBG) Log.d(TAG, "No default set, last payment service removed."); 541 } 542 } 543 544 void checkDefaultsLocked(int userId, List<ApduServiceInfo> services) { 545 ComponentName defaultPaymentService = 546 getDefaultServiceForCategory(userId, CardEmulation.CATEGORY_PAYMENT, false); 547 if (DBG) Log.d(TAG, "Current default: " + defaultPaymentService); 548 if (defaultPaymentService != null) { 549 // Validate the default is still installed and handling payment 550 ApduServiceInfo serviceInfo = mServiceCache.getService(userId, defaultPaymentService); 551 if (serviceInfo == null) { 552 Log.e(TAG, "Default payment service unexpectedly removed."); 553 onPaymentDefaultRemoved(userId, services); 554 } else if (!serviceInfo.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { 555 if (DBG) Log.d(TAG, "Default payment service had payment category removed"); 556 onPaymentDefaultRemoved(userId, services); 557 } else { 558 // Default still exists and handles the category, nothing do 559 if (DBG) Log.d(TAG, "Default payment service still ok."); 560 } 561 } else { 562 // A payment service may have been removed, leaving only one; 563 // in that case, automatically set that app as default. 564 setDefaultIfNeededLocked(userId, services); 565 } 566 updateFromSettingsLocked(userId); 567 } 568 569 @Override 570 public void onServicesUpdated(int userId, List<ApduServiceInfo> services) { 571 synchronized (mLock) { 572 if (ActivityManager.getCurrentUser() == userId) { 573 // Rebuild our internal data-structures 574 checkDefaultsLocked(userId, services); 575 generateAidTreeLocked(services); 576 generateAidCategoriesLocked(services); 577 generateAidCacheLocked(); 578 updateRoutingLocked(); 579 } else { 580 if (DBG) Log.d(TAG, "Ignoring update because it's not for the current user."); 581 } 582 } 583 } 584 585 public void invalidateCache(int currentUser) { 586 mServiceCache.invalidateCache(currentUser); 587 } 588 589 public void onNfcDisabled() { 590 synchronized (mLock) { 591 mNfcEnabled = false; 592 } 593 mServiceCache.onNfcDisabled(); 594 mRoutingManager.onNfccRoutingTableCleared(); 595 } 596 597 public void onNfcEnabled() { 598 synchronized (mLock) { 599 mNfcEnabled = true; 600 updateFromSettingsLocked(ActivityManager.getCurrentUser()); 601 } 602 mServiceCache.onNfcEnabled(); 603 } 604 605 String dumpEntry(Map.Entry<String, AidResolveInfo> entry) { 606 StringBuilder sb = new StringBuilder(); 607 sb.append(" \"" + entry.getKey() + "\"\n"); 608 ApduServiceInfo defaultService = entry.getValue().defaultService; 609 ComponentName defaultComponent = defaultService != null ? 610 defaultService.getComponent() : null; 611 612 for (ApduServiceInfo service : entry.getValue().services) { 613 sb.append(" "); 614 if (service.getComponent().equals(defaultComponent)) { 615 sb.append("*DEFAULT* "); 616 } 617 sb.append(service.getComponent() + 618 " (Description: " + service.getDescription() + ")\n"); 619 } 620 return sb.toString(); 621 } 622 623 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 624 mServiceCache.dump(fd, pw, args); 625 pw.println("AID cache entries: "); 626 for (Map.Entry<String, AidResolveInfo> entry : mAidCache.entrySet()) { 627 pw.println(dumpEntry(entry)); 628 } 629 pw.println("Category defaults: "); 630 for (Map.Entry<String, ComponentName> entry : mCategoryDefaults.entrySet()) { 631 pw.println(" " + entry.getKey() + "->" + entry.getValue()); 632 } 633 pw.println(""); 634 } 635 } 636