1package com.android.nfc.cardemulation;
2
3import android.app.ActivityManager;
4import android.content.ComponentName;
5import android.content.Context;
6import android.content.Intent;
7import android.database.ContentObserver;
8import android.net.Uri;
9import android.nfc.cardemulation.ApduServiceInfo;
10import android.nfc.cardemulation.CardEmulation;
11import android.nfc.cardemulation.ApduServiceInfo.AidGroup;
12import android.os.Handler;
13import android.os.Looper;
14import android.os.UserHandle;
15import android.provider.Settings;
16import android.util.Log;
17
18import com.google.android.collect.Maps;
19
20import java.io.FileDescriptor;
21import java.io.PrintWriter;
22import java.util.ArrayList;
23import java.util.HashMap;
24import java.util.HashSet;
25import java.util.List;
26import java.util.Map;
27import java.util.Set;
28import java.util.SortedMap;
29import java.util.TreeMap;
30
31public 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