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.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