1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.nfc.cardemulation;
18
19import android.app.ActivityManager;
20import android.content.ComponentName;
21import android.content.Context;
22import android.nfc.cardemulation.ApduServiceInfo;
23import android.nfc.cardemulation.CardEmulation;
24import android.util.Log;
25
26import com.google.android.collect.Maps;
27
28import java.io.FileDescriptor;
29import java.io.PrintWriter;
30import java.util.ArrayList;
31import java.util.Collection;
32import java.util.HashMap;
33import java.util.HashSet;
34import java.util.List;
35import java.util.Map;
36import java.util.NavigableMap;
37import java.util.PriorityQueue;
38import java.util.TreeMap;
39
40public class RegisteredAidCache {
41    static final String TAG = "RegisteredAidCache";
42
43    static final boolean DBG = false;
44
45    // mAidServices maps AIDs to services that have registered them.
46    // It's a TreeMap in order to be able to quickly select subsets
47    // of AIDs that conflict with each other.
48    final TreeMap<String, ArrayList<ServiceAidInfo>> mAidServices =
49            new TreeMap<String, ArrayList<ServiceAidInfo>>();
50
51    // mAidCache is a lookup table for quickly mapping an exact or prefix AID to one or
52    // more handling services. It differs from mAidServices in the sense that it
53    // has already accounted for defaults, and hence its return value
54    // is authoritative for the current set of services and defaults.
55    // It is only valid for the current user.
56    final TreeMap<String, AidResolveInfo> mAidCache = new TreeMap<String, AidResolveInfo>();
57
58    // Represents a single AID registration of a service
59    final class ServiceAidInfo {
60        ApduServiceInfo service;
61        String aid;
62        String category;
63
64        @Override
65        public String toString() {
66            return "ServiceAidInfo{" +
67                    "service=" + service.getComponent() +
68                    ", aid='" + aid + '\'' +
69                    ", category='" + category + '\'' +
70                    '}';
71        }
72
73        @Override
74        public boolean equals(Object o) {
75            if (this == o) return true;
76            if (o == null || getClass() != o.getClass()) return false;
77
78            ServiceAidInfo that = (ServiceAidInfo) o;
79
80            if (!aid.equals(that.aid)) return false;
81            if (!category.equals(that.category)) return false;
82            if (!service.equals(that.service)) return false;
83
84            return true;
85        }
86
87        @Override
88        public int hashCode() {
89            int result = service.hashCode();
90            result = 31 * result + aid.hashCode();
91            result = 31 * result + category.hashCode();
92            return result;
93        }
94    }
95
96    // Represents a list of services, an optional default and a category that
97    // an AID was resolved to.
98    final class AidResolveInfo {
99        List<ApduServiceInfo> services = new ArrayList<ApduServiceInfo>();
100        ApduServiceInfo defaultService = null;
101        String category = null;
102        boolean mustRoute = true; // Whether this AID should be routed at all
103
104        @Override
105        public String toString() {
106            return "AidResolveInfo{" +
107                    "services=" + services +
108                    ", defaultService=" + defaultService +
109                    ", category='" + category + '\'' +
110                    ", mustRoute=" + mustRoute +
111                    '}';
112        }
113    }
114
115    final AidResolveInfo EMPTY_RESOLVE_INFO = new AidResolveInfo();
116
117    final Context mContext;
118    final AidRoutingManager mRoutingManager;
119
120    final Object mLock = new Object();
121
122    ComponentName mPreferredPaymentService;
123    ComponentName mPreferredForegroundService;
124
125    boolean mNfcEnabled = false;
126    boolean mSupportsPrefixes = false;
127
128    public RegisteredAidCache(Context context) {
129        mContext = context;
130        mRoutingManager = new AidRoutingManager();
131        mPreferredPaymentService = null;
132        mPreferredForegroundService = null;
133        mSupportsPrefixes = mRoutingManager.supportsAidPrefixRouting();
134        if (mSupportsPrefixes) {
135            if (DBG) Log.d(TAG, "Controller supports AID prefix routing");
136        }
137    }
138
139    public AidResolveInfo resolveAid(String aid) {
140        synchronized (mLock) {
141            if (DBG) Log.d(TAG, "resolveAid: resolving AID " + aid);
142            if (aid.length() < 10) {
143                Log.e(TAG, "AID selected with fewer than 5 bytes.");
144                return EMPTY_RESOLVE_INFO;
145            }
146            AidResolveInfo resolveInfo = new AidResolveInfo();
147            if (mSupportsPrefixes) {
148                // Our AID cache may contain prefixes which also match this AID,
149                // so we must find all potential prefixes and merge the ResolveInfo
150                // of those prefixes plus any exact match in a single result.
151                String shortestAidMatch = aid.substring(0, 10); // Minimum AID length is 5 bytes
152                String longestAidMatch = aid + "*"; // Longest potential matching AID
153
154                if (DBG) Log.d(TAG, "Finding AID registrations in range [" + shortestAidMatch +
155                        " - " + longestAidMatch + "]");
156                NavigableMap<String, AidResolveInfo> matchingAids =
157                        mAidCache.subMap(shortestAidMatch, true, longestAidMatch, true);
158
159                resolveInfo.category = CardEmulation.CATEGORY_OTHER;
160                for (Map.Entry<String, AidResolveInfo> entry : matchingAids.entrySet()) {
161                    boolean isPrefix = isPrefix(entry.getKey());
162                    String entryAid = isPrefix ? entry.getKey().substring(0,
163                            entry.getKey().length() - 1) : entry.getKey(); // Cut off '*' if prefix
164                    if (entryAid.equalsIgnoreCase(aid) || (isPrefix && aid.startsWith(entryAid))) {
165                        if (DBG) Log.d(TAG, "resolveAid: AID " + entryAid + " matches.");
166                        AidResolveInfo entryResolveInfo = entry.getValue();
167                        if (entryResolveInfo.defaultService != null) {
168                            if (resolveInfo.defaultService != null) {
169                                // This shouldn't happen; for every prefix we have only one
170                                // default service.
171                                Log.e(TAG, "Different defaults for conflicting AIDs!");
172                            }
173                            resolveInfo.defaultService = entryResolveInfo.defaultService;
174                            resolveInfo.category = entryResolveInfo.category;
175                        }
176                        for (ApduServiceInfo serviceInfo : entryResolveInfo.services) {
177                            if (!resolveInfo.services.contains(serviceInfo)) {
178                                resolveInfo.services.add(serviceInfo);
179                            }
180                        }
181                    }
182                }
183            } else {
184                resolveInfo = mAidCache.get(aid);
185            }
186            if (DBG) Log.d(TAG, "Resolved to: " + resolveInfo);
187            return resolveInfo;
188        }
189    }
190
191    public boolean supportsAidPrefixRegistration() {
192        return mSupportsPrefixes;
193    }
194
195    public boolean isDefaultServiceForAid(int userId, ComponentName service, String aid) {
196        AidResolveInfo resolveInfo = resolveAid(aid);
197        if (resolveInfo == null || resolveInfo.services == null ||
198                resolveInfo.services.size() == 0) {
199            return false;
200        }
201
202        if (resolveInfo.defaultService != null) {
203            return service.equals(resolveInfo.defaultService.getComponent());
204        } else if (resolveInfo.services.size() == 1) {
205            return service.equals(resolveInfo.services.get(0).getComponent());
206        } else {
207            // More than one service, not the default
208            return false;
209        }
210    }
211
212    /**
213     * Resolves a conflict between multiple services handling the same
214     * AIDs. Note that the AID itself is not an input to the decision
215     * process - the algorithm just looks at the competing services
216     * and what preferences the user has indicated. In short, it works like
217     * this:
218     *
219     * 1) If there is a preferred foreground service, that service wins
220     * 2) Else, if there is a preferred payment service, that service wins
221     * 3) Else, if there is no winner, and all conflicting services will be
222     *    in the list of resolved services.
223     */
224     AidResolveInfo resolveAidConflictLocked(Collection<ServiceAidInfo> conflictingServices,
225                                             boolean makeSingleServiceDefault) {
226        if (conflictingServices == null || conflictingServices.size() == 0) {
227            Log.e(TAG, "resolveAidConflict: No services passed in.");
228            return null;
229        }
230        AidResolveInfo resolveInfo = new AidResolveInfo();
231        resolveInfo.category = CardEmulation.CATEGORY_OTHER;
232
233        ApduServiceInfo matchedForeground = null;
234        ApduServiceInfo matchedPayment = null;
235        for (ServiceAidInfo serviceAidInfo : conflictingServices) {
236            boolean serviceClaimsPaymentAid =
237                    CardEmulation.CATEGORY_PAYMENT.equals(serviceAidInfo.category);
238            if (serviceAidInfo.service.getComponent().equals(mPreferredForegroundService)) {
239                resolveInfo.services.add(serviceAidInfo.service);
240                if (serviceClaimsPaymentAid) {
241                    resolveInfo.category = CardEmulation.CATEGORY_PAYMENT;
242                }
243                matchedForeground = serviceAidInfo.service;
244            } else if (serviceAidInfo.service.getComponent().equals(mPreferredPaymentService) &&
245                    serviceClaimsPaymentAid) {
246                resolveInfo.services.add(serviceAidInfo.service);
247                resolveInfo.category = CardEmulation.CATEGORY_PAYMENT;
248                matchedPayment = serviceAidInfo.service;
249            } else {
250                if (serviceClaimsPaymentAid) {
251                    // If this service claims it's a payment AID, don't route it,
252                    // because it's not the default. Otherwise, add it to the list
253                    // but not as default.
254                    if (DBG) Log.d(TAG, "resolveAidLocked: (Ignoring handling service " +
255                            serviceAidInfo.service.getComponent() +
256                            " because it's not the payment default.)");
257                } else {
258                    resolveInfo.services.add(serviceAidInfo.service);
259                }
260            }
261        }
262        if (matchedForeground != null) {
263            // 1st priority: if the foreground app prefers a service,
264            // and that service asks for the AID, that service gets it
265            if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to foreground preferred " +
266                    matchedForeground);
267            resolveInfo.defaultService = matchedForeground;
268        } else if (matchedPayment != null) {
269            // 2nd priority: if there is a preferred payment service,
270            // and that service claims this as a payment AID, that service gets it
271            if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to payment default " +
272                    "default " + matchedPayment);
273            resolveInfo.defaultService = matchedPayment;
274        } else {
275            if (resolveInfo.services.size() == 1 && makeSingleServiceDefault) {
276                if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: making single handling service " +
277                        resolveInfo.services.get(0).getComponent() + " default.");
278                resolveInfo.defaultService = resolveInfo.services.get(0);
279            } else {
280                // Nothing to do, all services already in list
281                if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to all matching services");
282            }
283        }
284        return resolveInfo;
285    }
286
287    class DefaultServiceInfo {
288        ServiceAidInfo paymentDefault;
289        ServiceAidInfo foregroundDefault;
290    }
291
292    DefaultServiceInfo findDefaultServices(ArrayList<ServiceAidInfo> serviceAidInfos) {
293        DefaultServiceInfo defaultServiceInfo = new DefaultServiceInfo();
294
295        for (ServiceAidInfo serviceAidInfo : serviceAidInfos) {
296            boolean serviceClaimsPaymentAid =
297                    CardEmulation.CATEGORY_PAYMENT.equals(serviceAidInfo.category);
298            if (serviceAidInfo.service.getComponent().equals(mPreferredForegroundService)) {
299                defaultServiceInfo.foregroundDefault = serviceAidInfo;
300            } else if (serviceAidInfo.service.getComponent().equals(mPreferredPaymentService) &&
301                    serviceClaimsPaymentAid) {
302                defaultServiceInfo.paymentDefault = serviceAidInfo;
303            }
304        }
305        return defaultServiceInfo;
306    }
307
308    AidResolveInfo resolvePrefixAidConflictLocked(ArrayList<ServiceAidInfo> prefixServices,
309                                                  ArrayList<ServiceAidInfo> conflictingServices) {
310        // Find defaults among the prefix services themselves
311        DefaultServiceInfo prefixDefaultInfo = findDefaultServices(prefixServices);
312
313        // Find any defaults among the children
314        DefaultServiceInfo conflictingDefaultInfo = findDefaultServices(conflictingServices);
315
316        // Three conditions under which the prefix root AID gets to be the default
317        // 1. A service registering the prefix root AID is the current foreground preferred
318        // 2. A service registering the prefix root AID is the current tap & pay default AND
319        //    no child is the current foreground preferred
320        // 3. There is only one service for the prefix root AID, and there are no children
321        if (prefixDefaultInfo.foregroundDefault != null) {
322            if (DBG) Log.d(TAG, "Prefix AID service " +
323                    prefixDefaultInfo.foregroundDefault.service.getComponent() + " has foreground" +
324                    " preference, ignoring conflicting AIDs.");
325            // Foreground default trumps any conflicting services, treat as normal AID conflict
326            // and ignore children
327            return resolveAidConflictLocked(prefixServices, true);
328        } else if (prefixDefaultInfo.paymentDefault != null) {
329            // Check if any of the conflicting services is foreground default
330            if (conflictingDefaultInfo.foregroundDefault != null) {
331                // Conflicting AID registration is in foreground, trumps prefix tap&pay default
332                if (DBG) Log.d(TAG, "One of the conflicting AID registrations is foreground " +
333                        "preferred, ignoring prefix.");
334                return EMPTY_RESOLVE_INFO;
335            } else {
336                // Prefix service is tap&pay default, treat as normal AID conflict for just prefix
337                if (DBG) Log.d(TAG, "Prefix AID service " +
338                        prefixDefaultInfo.paymentDefault.service.getComponent() + " is payment" +
339                        " default, ignoring conflicting AIDs.");
340                return resolveAidConflictLocked(prefixServices, true);
341            }
342        } else {
343            if (conflictingDefaultInfo.foregroundDefault != null ||
344                    conflictingDefaultInfo.paymentDefault != null) {
345                if (DBG) Log.d(TAG, "One of the conflicting AID registrations is either payment " +
346                        "default or foreground preferred, ignoring prefix.");
347                return EMPTY_RESOLVE_INFO;
348            } else {
349                // No children that are preferred; add all services of the root
350                // make single service default if no children are present
351                if (DBG) Log.d(TAG, "No service has preference, adding all.");
352                return resolveAidConflictLocked(prefixServices, conflictingServices.isEmpty());
353            }
354        }
355    }
356
357    void generateServiceMapLocked(List<ApduServiceInfo> services) {
358        // Easiest is to just build the entire tree again
359        mAidServices.clear();
360        for (ApduServiceInfo service : services) {
361            if (DBG) Log.d(TAG, "generateServiceMap component: " + service.getComponent());
362            for (String aid : service.getAids()) {
363                if (!CardEmulation.isValidAid(aid)) {
364                    Log.e(TAG, "Aid " + aid + " is not valid.");
365                    continue;
366                }
367                if (aid.endsWith("*") && !supportsAidPrefixRegistration()) {
368                    Log.e(TAG, "Prefix AID " + aid + " ignored on device that doesn't support it.");
369                    continue;
370                }
371                ServiceAidInfo serviceAidInfo = new ServiceAidInfo();
372                serviceAidInfo.aid = aid.toUpperCase();
373                serviceAidInfo.service = service;
374                serviceAidInfo.category = service.getCategoryForAid(aid);
375
376                if (mAidServices.containsKey(serviceAidInfo.aid)) {
377                    final ArrayList<ServiceAidInfo> serviceAidInfos =
378                            mAidServices.get(serviceAidInfo.aid);
379                    serviceAidInfos.add(serviceAidInfo);
380                } else {
381                    final ArrayList<ServiceAidInfo> serviceAidInfos =
382                            new ArrayList<ServiceAidInfo>();
383                    serviceAidInfos.add(serviceAidInfo);
384                    mAidServices.put(serviceAidInfo.aid, serviceAidInfos);
385                }
386            }
387        }
388    }
389
390    static boolean isPrefix(String aid) {
391        return aid.endsWith("*");
392    }
393
394    final class PrefixConflicts {
395        NavigableMap<String, ArrayList<ServiceAidInfo>> conflictMap;
396        final ArrayList<ServiceAidInfo> services = new ArrayList<ServiceAidInfo>();
397        final HashSet<String> aids = new HashSet<String>();
398    }
399
400    PrefixConflicts findConflictsForPrefixLocked(String prefixAid) {
401        PrefixConflicts prefixConflicts = new PrefixConflicts();
402        String plainAid = prefixAid.substring(0, prefixAid.length() - 1); // Cut off "*"
403        String lastAidWithPrefix = String.format("%-32s", plainAid).replace(' ', 'F');
404        if (DBG) Log.d(TAG, "Finding AIDs in range [" + plainAid + " - " +
405                lastAidWithPrefix + "]");
406        prefixConflicts.conflictMap =
407                mAidServices.subMap(plainAid, true, lastAidWithPrefix, true);
408        for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
409                prefixConflicts.conflictMap.entrySet()) {
410            if (!entry.getKey().equalsIgnoreCase(prefixAid)) {
411                if (DBG)
412                    Log.d(TAG, "AID " + entry.getKey() + " conflicts with prefix; " +
413                            " adding handling services for conflict resolution.");
414                prefixConflicts.services.addAll(entry.getValue());
415                prefixConflicts.aids.add(entry.getKey());
416            }
417        }
418        return prefixConflicts;
419    }
420
421    void generateAidCacheLocked() {
422        mAidCache.clear();
423        // Get all exact and prefix AIDs in an ordered list
424        PriorityQueue<String> aidsToResolve = new PriorityQueue<String>(mAidServices.keySet());
425
426        while (!aidsToResolve.isEmpty()) {
427            final ArrayList<String> resolvedAids = new ArrayList<String>();
428
429            String aidToResolve = aidsToResolve.peek();
430            // Because of the lexicographical ordering, all following AIDs either start with the
431            // same bytes and are longer, or start with different bytes.
432
433            // A special case is if another service registered the same AID as a prefix, in
434            // which case we want to start with that AID, since it conflicts with this one
435            if (aidsToResolve.contains(aidToResolve + "*")) {
436                aidToResolve = aidToResolve + "*";
437            }
438            if (DBG) Log.d(TAG, "generateAidCacheLocked: starting with aid " + aidToResolve);
439
440            if (isPrefix(aidToResolve)) {
441                // This AID itself is a prefix; let's consider this prefix as the "root",
442                // and all conflicting AIDs as its children.
443                // For example, if "A000000003*" is the prefix root,
444                // "A000000003", "A00000000301*", "A0000000030102" are all conflicting children AIDs
445                final ArrayList<ServiceAidInfo> prefixServices = new ArrayList<ServiceAidInfo>(
446                        mAidServices.get(aidToResolve));
447
448                // Find all conflicting children services
449                PrefixConflicts prefixConflicts = findConflictsForPrefixLocked(aidToResolve);
450
451                // Resolve conflicts
452                AidResolveInfo resolveInfo = resolvePrefixAidConflictLocked(prefixServices,
453                        prefixConflicts.services);
454                mAidCache.put(aidToResolve, resolveInfo);
455                resolvedAids.add(aidToResolve);
456                if (resolveInfo.defaultService != null) {
457                    // This prefix is the default; therefore, AIDs of all conflicting children
458                    // will no longer be evaluated.
459                    resolvedAids.addAll(prefixConflicts.aids);
460                } else if (resolveInfo.services.size() > 0) {
461                    // This means we don't have a default for this prefix and all its
462                    // conflicting children. So, for all conflicting AIDs, just add
463                    // all handling services without setting a default
464                    boolean foundChildService = false;
465                    for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
466                            prefixConflicts.conflictMap.entrySet()) {
467                        if (!entry.getKey().equalsIgnoreCase(aidToResolve)) {
468                            if (DBG)
469                                Log.d(TAG, "AID " + entry.getKey() + " shared with prefix; " +
470                                        " adding all handling services.");
471                            AidResolveInfo childResolveInfo = resolveAidConflictLocked(
472                                    entry.getValue(), false);
473                            // Special case: in this case all children AIDs must be routed to the
474                            // host, so we can ask the user which service is preferred.
475                            // Since these are all "children" of the prefix, they don't need
476                            // to be routed, since the prefix will already get routed to the host
477                            childResolveInfo.mustRoute = false;
478                            mAidCache.put(entry.getKey(),childResolveInfo);
479                            resolvedAids.add(entry.getKey());
480                            foundChildService |= !childResolveInfo.services.isEmpty();
481                        }
482                    }
483                    // Special case: if in the end we didn't add any children services,
484                    // and the prefix has only one service, make that default
485                    if (!foundChildService && resolveInfo.services.size() == 1) {
486                        resolveInfo.defaultService = resolveInfo.services.get(0);
487                    }
488                } else {
489                    // This prefix is not handled at all; we will evaluate
490                    // the children separately in next passes.
491                }
492            } else {
493                // Exact AID and no other conflicting AID registrations present
494                // This is true because aidsToResolve is lexicographically ordered, and
495                // so by necessity all other AIDs are different than this AID or longer.
496                if (DBG) Log.d(TAG, "Exact AID, resolving.");
497                final ArrayList<ServiceAidInfo> conflictingServiceInfos =
498                        new ArrayList<ServiceAidInfo>(mAidServices.get(aidToResolve));
499                mAidCache.put(aidToResolve, resolveAidConflictLocked(conflictingServiceInfos, true));
500                resolvedAids.add(aidToResolve);
501            }
502
503            // Remove the AIDs we resolved from the list of AIDs to resolve
504            if (DBG) Log.d(TAG, "AIDs: " + resolvedAids + " were resolved.");
505            aidsToResolve.removeAll(resolvedAids);
506            resolvedAids.clear();
507        }
508
509        updateRoutingLocked();
510    }
511
512    void updateRoutingLocked() {
513        if (!mNfcEnabled) {
514            if (DBG) Log.d(TAG, "Not updating routing table because NFC is off.");
515            return;
516        }
517        final HashMap<String, Boolean> routingEntries = Maps.newHashMap();
518        // For each AID, find interested services
519        for (Map.Entry<String, AidResolveInfo> aidEntry:
520                mAidCache.entrySet()) {
521            String aid = aidEntry.getKey();
522            AidResolveInfo resolveInfo = aidEntry.getValue();
523            if (!resolveInfo.mustRoute) {
524                if (DBG) Log.d(TAG, "Not routing AID " + aid + " on request.");
525                continue;
526            }
527            if (resolveInfo.services.size() == 0) {
528                // No interested services
529            } else if (resolveInfo.defaultService != null) {
530                // There is a default service set, route to where that service resides -
531                // either on the host (HCE) or on an SE.
532                routingEntries.put(aid, resolveInfo.defaultService.isOnHost());
533            } else if (resolveInfo.services.size() == 1) {
534                // Only one service, but not the default, must route to host
535                // to ask the user to choose one.
536                routingEntries.put(aid, true);
537            } else if (resolveInfo.services.size() > 1) {
538                // Multiple services, need to route to host to ask
539                routingEntries.put(aid, true);
540            }
541        }
542        mRoutingManager.configureRouting(routingEntries);
543    }
544
545    public void onServicesUpdated(int userId, List<ApduServiceInfo> services) {
546        if (DBG) Log.d(TAG, "onServicesUpdated");
547        synchronized (mLock) {
548            if (ActivityManager.getCurrentUser() == userId) {
549                // Rebuild our internal data-structures
550                generateServiceMapLocked(services);
551                generateAidCacheLocked();
552            } else {
553                if (DBG) Log.d(TAG, "Ignoring update because it's not for the current user.");
554            }
555        }
556    }
557
558    public void onPreferredPaymentServiceChanged(ComponentName service) {
559        if (DBG) Log.d(TAG, "Preferred payment service changed.");
560       synchronized (mLock) {
561           mPreferredPaymentService = service;
562           generateAidCacheLocked();
563       }
564    }
565
566    public void onPreferredForegroundServiceChanged(ComponentName service) {
567        if (DBG) Log.d(TAG, "Preferred foreground service changed.");
568        synchronized (mLock) {
569            mPreferredForegroundService = service;
570            generateAidCacheLocked();
571        }
572    }
573
574    public void onNfcDisabled() {
575        synchronized (mLock) {
576            mNfcEnabled = false;
577        }
578        mRoutingManager.onNfccRoutingTableCleared();
579    }
580
581    public void onNfcEnabled() {
582        synchronized (mLock) {
583            mNfcEnabled = true;
584            updateRoutingLocked();
585        }
586    }
587
588    String dumpEntry(Map.Entry<String, AidResolveInfo> entry) {
589        StringBuilder sb = new StringBuilder();
590        String category = entry.getValue().category;
591        ApduServiceInfo defaultServiceInfo = entry.getValue().defaultService;
592        sb.append("    \"" + entry.getKey() + "\" (category: " + category + ")\n");
593        ComponentName defaultComponent = defaultServiceInfo != null ?
594                defaultServiceInfo.getComponent() : null;
595
596        for (ApduServiceInfo serviceInfo : entry.getValue().services) {
597            sb.append("        ");
598            if (serviceInfo.getComponent().equals(defaultComponent)) {
599                sb.append("*DEFAULT* ");
600            }
601            sb.append(serviceInfo.getComponent() +
602                    " (Description: " + serviceInfo.getDescription() + ")\n");
603        }
604        return sb.toString();
605    }
606
607    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
608        pw.println("    AID cache entries: ");
609        for (Map.Entry<String, AidResolveInfo> entry : mAidCache.entrySet()) {
610            pw.println(dumpEntry(entry));
611        }
612        pw.println("    Service preferred by foreground app: " + mPreferredForegroundService);
613        pw.println("    Preferred payment service: " + mPreferredPaymentService);
614        pw.println("");
615        mRoutingManager.dump(fd, pw, args);
616        pw.println("");
617    }
618}
619