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 
17 package com.android.nfc.cardemulation;
18 
19 import android.app.ActivityManager;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.nfc.cardemulation.ApduServiceInfo;
23 import android.nfc.cardemulation.CardEmulation;
24 import android.util.Log;
25 
26 import com.google.android.collect.Maps;
27 import java.util.Collections;
28 import java.io.FileDescriptor;
29 import java.io.PrintWriter;
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.NavigableMap;
37 import java.util.PriorityQueue;
38 import java.util.TreeMap;
39 
40 public class RegisteredAidCache {
41     static final String TAG = "RegisteredAidCache";
42 
43     static final boolean DBG = false;
44 
45     static final int AID_ROUTE_QUAL_SUBSET = 0x20;
46     static final int AID_ROUTE_QUAL_PREFIX = 0x10;
47 
48     // mAidServices maps AIDs to services that have registered them.
49     // It's a TreeMap in order to be able to quickly select subsets
50     // of AIDs that conflict with each other.
51     final TreeMap<String, ArrayList<ServiceAidInfo>> mAidServices =
52             new TreeMap<String, ArrayList<ServiceAidInfo>>();
53 
54     // mAidCache is a lookup table for quickly mapping an exact or prefix or subset AID
55     // to one or more handling services. It differs from mAidServices in the sense that it
56     // has already accounted for defaults, and hence its return value
57     // is authoritative for the current set of services and defaults.
58     // It is only valid for the current user.
59     final TreeMap<String, AidResolveInfo> mAidCache = new TreeMap<String, AidResolveInfo>();
60 
61     // Represents a single AID registration of a service
62     final class ServiceAidInfo {
63         ApduServiceInfo service;
64         String aid;
65         String category;
66 
67         @Override
toString()68         public String toString() {
69             return "ServiceAidInfo{" +
70                     "service=" + service.getComponent() +
71                     ", aid='" + aid + '\'' +
72                     ", category='" + category + '\'' +
73                     '}';
74         }
75 
76         @Override
equals(Object o)77         public boolean equals(Object o) {
78             if (this == o) return true;
79             if (o == null || getClass() != o.getClass()) return false;
80 
81             ServiceAidInfo that = (ServiceAidInfo) o;
82 
83             if (!aid.equals(that.aid)) return false;
84             if (!category.equals(that.category)) return false;
85             if (!service.equals(that.service)) return false;
86 
87             return true;
88         }
89 
90         @Override
hashCode()91         public int hashCode() {
92             int result = service.hashCode();
93             result = 31 * result + aid.hashCode();
94             result = 31 * result + category.hashCode();
95             return result;
96         }
97     }
98 
99     // Represents a list of services, an optional default and a category that
100     // an AID was resolved to.
101     final class AidResolveInfo {
102         List<ApduServiceInfo> services = new ArrayList<ApduServiceInfo>();
103         ApduServiceInfo defaultService = null;
104         String category = null;
105         boolean mustRoute = true; // Whether this AID should be routed at all
106         ReslovedPrefixConflictAid prefixInfo = null;
107         @Override
toString()108         public String toString() {
109             return "AidResolveInfo{" +
110                     "services=" + services +
111                     ", defaultService=" + defaultService +
112                     ", category='" + category + '\'' +
113                     ", mustRoute=" + mustRoute +
114                     '}';
115         }
116     }
117 
118     final AidResolveInfo EMPTY_RESOLVE_INFO = new AidResolveInfo();
119 
120     final Context mContext;
121     final AidRoutingManager mRoutingManager;
122 
123     final Object mLock = new Object();
124 
125     ComponentName mPreferredPaymentService;
126     ComponentName mPreferredForegroundService;
127 
128     boolean mNfcEnabled = false;
129     boolean mSupportsPrefixes = false;
130     boolean mSupportsSubset = false;
131 
RegisteredAidCache(Context context)132     public RegisteredAidCache(Context context) {
133         mContext = context;
134         mRoutingManager = new AidRoutingManager();
135         mPreferredPaymentService = null;
136         mPreferredForegroundService = null;
137         mSupportsPrefixes = mRoutingManager.supportsAidPrefixRouting();
138         mSupportsSubset   = mRoutingManager.supportsAidSubsetRouting();
139         if (mSupportsPrefixes) {
140             if (DBG) Log.d(TAG, "Controller supports AID prefix routing");
141         }
142         if (mSupportsSubset) {
143             if (DBG) Log.d(TAG, "Controller supports AID subset routing");
144         }
145     }
146 
resolveAid(String aid)147     public AidResolveInfo resolveAid(String aid) {
148         synchronized (mLock) {
149             if (DBG) Log.d(TAG, "resolveAid: resolving AID " + aid);
150             if (aid.length() < 10) {
151                 Log.e(TAG, "AID selected with fewer than 5 bytes.");
152                 return EMPTY_RESOLVE_INFO;
153             }
154             AidResolveInfo resolveInfo = new AidResolveInfo();
155             if (mSupportsPrefixes || mSupportsSubset) {
156                 // Our AID cache may contain prefixes/subset which also match this AID,
157                 // so we must find all potential prefixes or suffixes and merge the ResolveInfo
158                 // of those prefixes plus any exact match in a single result.
159                 String shortestAidMatch = aid.substring(0, 10); // Minimum AID length is 5 bytes
160                 String longestAidMatch = String.format("%-32s", aid).replace(' ', 'F');
161 
162 
163                 if (DBG) Log.d(TAG, "Finding AID registrations in range [" + shortestAidMatch +
164                         " - " + longestAidMatch + "]");
165                 NavigableMap<String, AidResolveInfo> matchingAids =
166                         mAidCache.subMap(shortestAidMatch, true, longestAidMatch, true);
167 
168                 resolveInfo.category = CardEmulation.CATEGORY_OTHER;
169                 for (Map.Entry<String, AidResolveInfo> entry : matchingAids.entrySet()) {
170                     boolean isPrefix = isPrefix(entry.getKey());
171                     boolean isSubset = isSubset(entry.getKey());
172                     String entryAid = (isPrefix || isSubset) ? entry.getKey().substring(0,
173                             entry.getKey().length() - 1):entry.getKey(); // Cut off '*' if prefix
174                     if (entryAid.equalsIgnoreCase(aid) || (isPrefix && aid.startsWith(entryAid))
175                             || (isSubset && entryAid.startsWith(aid))) {
176                         if (DBG) Log.d(TAG, "resolveAid: AID " + entry.getKey() + " matches.");
177                         AidResolveInfo entryResolveInfo = entry.getValue();
178                         if (entryResolveInfo.defaultService != null) {
179                             if (resolveInfo.defaultService != null) {
180                                 // This shouldn't happen; for every prefix we have only one
181                                 // default service.
182                                 Log.e(TAG, "Different defaults for conflicting AIDs!");
183                             }
184                             resolveInfo.defaultService = entryResolveInfo.defaultService;
185                             resolveInfo.category = entryResolveInfo.category;
186                         }
187                         for (ApduServiceInfo serviceInfo : entryResolveInfo.services) {
188                             if (!resolveInfo.services.contains(serviceInfo)) {
189                                 resolveInfo.services.add(serviceInfo);
190                             }
191                         }
192                     }
193                 }
194             } else {
195                 resolveInfo = mAidCache.get(aid);
196             }
197             if (DBG) Log.d(TAG, "Resolved to: " + resolveInfo);
198             return resolveInfo;
199         }
200     }
201 
supportsAidPrefixRegistration()202     public boolean supportsAidPrefixRegistration() {
203         return mSupportsPrefixes;
204     }
205 
supportsAidSubsetRegistration()206     public boolean supportsAidSubsetRegistration() {
207         return mSupportsSubset;
208     }
209 
isDefaultServiceForAid(int userId, ComponentName service, String aid)210     public boolean isDefaultServiceForAid(int userId, ComponentName service, String aid) {
211         AidResolveInfo resolveInfo = resolveAid(aid);
212         if (resolveInfo == null || resolveInfo.services == null ||
213                 resolveInfo.services.size() == 0) {
214             return false;
215         }
216 
217         if (resolveInfo.defaultService != null) {
218             return service.equals(resolveInfo.defaultService.getComponent());
219         } else if (resolveInfo.services.size() == 1) {
220             return service.equals(resolveInfo.services.get(0).getComponent());
221         } else {
222             // More than one service, not the default
223             return false;
224         }
225     }
226 
227     /**
228      * Resolves a conflict between multiple services handling the same
229      * AIDs. Note that the AID itself is not an input to the decision
230      * process - the algorithm just looks at the competing services
231      * and what preferences the user has indicated. In short, it works like
232      * this:
233      *
234      * 1) If there is a preferred foreground service, that service wins
235      * 2) Else, if there is a preferred payment service, that service wins
236      * 3) Else, if there is no winner, and all conflicting services will be
237      *    in the list of resolved services.
238      */
resolveAidConflictLocked(Collection<ServiceAidInfo> conflictingServices, boolean makeSingleServiceDefault)239      AidResolveInfo resolveAidConflictLocked(Collection<ServiceAidInfo> conflictingServices,
240                                              boolean makeSingleServiceDefault) {
241         if (conflictingServices == null || conflictingServices.size() == 0) {
242             Log.e(TAG, "resolveAidConflict: No services passed in.");
243             return null;
244         }
245         AidResolveInfo resolveInfo = new AidResolveInfo();
246         resolveInfo.category = CardEmulation.CATEGORY_OTHER;
247 
248         ApduServiceInfo matchedForeground = null;
249         ApduServiceInfo matchedPayment = null;
250         for (ServiceAidInfo serviceAidInfo : conflictingServices) {
251             boolean serviceClaimsPaymentAid =
252                     CardEmulation.CATEGORY_PAYMENT.equals(serviceAidInfo.category);
253             if (serviceAidInfo.service.getComponent().equals(mPreferredForegroundService)) {
254                 resolveInfo.services.add(serviceAidInfo.service);
255                 if (serviceClaimsPaymentAid) {
256                     resolveInfo.category = CardEmulation.CATEGORY_PAYMENT;
257                 }
258                 matchedForeground = serviceAidInfo.service;
259             } else if (serviceAidInfo.service.getComponent().equals(mPreferredPaymentService) &&
260                     serviceClaimsPaymentAid) {
261                 resolveInfo.services.add(serviceAidInfo.service);
262                 resolveInfo.category = CardEmulation.CATEGORY_PAYMENT;
263                 matchedPayment = serviceAidInfo.service;
264             } else {
265                 if (serviceClaimsPaymentAid) {
266                     // If this service claims it's a payment AID, don't route it,
267                     // because it's not the default. Otherwise, add it to the list
268                     // but not as default.
269                     if (DBG) Log.d(TAG, "resolveAidLocked: (Ignoring handling service " +
270                             serviceAidInfo.service.getComponent() +
271                             " because it's not the payment default.)");
272                 } else {
273                     resolveInfo.services.add(serviceAidInfo.service);
274                 }
275             }
276         }
277         if (matchedForeground != null) {
278             // 1st priority: if the foreground app prefers a service,
279             // and that service asks for the AID, that service gets it
280             if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to foreground preferred " +
281                     matchedForeground);
282             resolveInfo.defaultService = matchedForeground;
283         } else if (matchedPayment != null) {
284             // 2nd priority: if there is a preferred payment service,
285             // and that service claims this as a payment AID, that service gets it
286             if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to payment default " +
287                     "default " + matchedPayment);
288             resolveInfo.defaultService = matchedPayment;
289         } else {
290             if (resolveInfo.services.size() == 1 && makeSingleServiceDefault) {
291                 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: making single handling service " +
292                         resolveInfo.services.get(0).getComponent() + " default.");
293                 resolveInfo.defaultService = resolveInfo.services.get(0);
294             } else {
295                 // Nothing to do, all services already in list
296                 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to all matching services");
297             }
298         }
299         return resolveInfo;
300     }
301 
302     class DefaultServiceInfo {
303         ServiceAidInfo paymentDefault;
304         ServiceAidInfo foregroundDefault;
305     }
306 
findDefaultServices(ArrayList<ServiceAidInfo> serviceAidInfos)307     DefaultServiceInfo findDefaultServices(ArrayList<ServiceAidInfo> serviceAidInfos) {
308         DefaultServiceInfo defaultServiceInfo = new DefaultServiceInfo();
309 
310         for (ServiceAidInfo serviceAidInfo : serviceAidInfos) {
311             boolean serviceClaimsPaymentAid =
312                     CardEmulation.CATEGORY_PAYMENT.equals(serviceAidInfo.category);
313             if (serviceAidInfo.service.getComponent().equals(mPreferredForegroundService)) {
314                 defaultServiceInfo.foregroundDefault = serviceAidInfo;
315             } else if (serviceAidInfo.service.getComponent().equals(mPreferredPaymentService) &&
316                     serviceClaimsPaymentAid) {
317                 defaultServiceInfo.paymentDefault = serviceAidInfo;
318             }
319         }
320         return defaultServiceInfo;
321     }
322 
resolveAidConflictLocked(ArrayList<ServiceAidInfo> aidServices, ArrayList<ServiceAidInfo> conflictingServices)323     AidResolveInfo resolveAidConflictLocked(ArrayList<ServiceAidInfo> aidServices,
324                                                   ArrayList<ServiceAidInfo> conflictingServices) {
325         // Find defaults among the root AID services themselves
326         DefaultServiceInfo aidDefaultInfo = findDefaultServices(aidServices);
327 
328         // Find any defaults among the children
329         DefaultServiceInfo conflictingDefaultInfo = findDefaultServices(conflictingServices);
330         AidResolveInfo resolveinfo;
331         // Three conditions under which the root AID gets to be the default
332         // 1. A service registering the root AID is the current foreground preferred
333         // 2. A service registering the root AID is the current tap & pay default AND
334         //    no child is the current foreground preferred
335         // 3. There is only one service for the root AID, and there are no children
336         if (aidDefaultInfo.foregroundDefault != null) {
337             if (DBG) Log.d(TAG, "Prefix AID service " +
338                     aidDefaultInfo.foregroundDefault.service.getComponent() + " has foreground" +
339                     " preference, ignoring conflicting AIDs.");
340             // Foreground default trumps any conflicting services, treat as normal AID conflict
341             // and ignore children
342             resolveinfo = resolveAidConflictLocked(aidServices, true);
343             //If the AID is subsetAID check for prefix in same service.
344             if (isSubset(aidServices.get(0).aid)) {
345                 resolveinfo.prefixInfo = findPrefixConflictForSubsetAid(aidServices.get(0).aid ,
346                         new ArrayList<ApduServiceInfo>(){{add(resolveinfo.defaultService);}},true);
347             }
348              return resolveinfo;
349         } else if (aidDefaultInfo.paymentDefault != null) {
350             // Check if any of the conflicting services is foreground default
351             if (conflictingDefaultInfo.foregroundDefault != null) {
352                 // Conflicting AID registration is in foreground, trumps prefix tap&pay default
353                 if (DBG) Log.d(TAG, "One of the conflicting AID registrations is foreground " +
354                         "preferred, ignoring prefix.");
355                 return EMPTY_RESOLVE_INFO;
356             } else {
357                 // Prefix service is tap&pay default, treat as normal AID conflict for just prefix
358                 if (DBG) Log.d(TAG, "Prefix AID service " +
359                     aidDefaultInfo.paymentDefault.service.getComponent() + " is payment" +
360                         " default, ignoring conflicting AIDs.");
361                 resolveinfo = resolveAidConflictLocked(aidServices, true);
362                 //If the AID is subsetAID check for prefix in same service.
363                 if (isSubset(aidServices.get(0).aid)) {
364                     resolveinfo.prefixInfo = findPrefixConflictForSubsetAid(aidServices.get(0).aid ,
365                         new ArrayList<ApduServiceInfo>(){{add(resolveinfo.defaultService);}},true);
366                 }
367                 return resolveinfo;
368             }
369         } else {
370             if (conflictingDefaultInfo.foregroundDefault != null ||
371                     conflictingDefaultInfo.paymentDefault != null) {
372                 if (DBG) Log.d(TAG, "One of the conflicting AID registrations is either payment " +
373                         "default or foreground preferred, ignoring prefix.");
374                 return EMPTY_RESOLVE_INFO;
375             } else {
376                 // No children that are preferred; add all services of the root
377                 // make single service default if no children are present
378                 if (DBG) Log.d(TAG, "No service has preference, adding all.");
379                 resolveinfo = resolveAidConflictLocked(aidServices, conflictingServices.isEmpty());
380                 //If the AID is subsetAID check for conflicting prefix in all
381                 //conflciting services and root services.
382                 if (isSubset(aidServices.get(0).aid)) {
383                     ArrayList <ApduServiceInfo> apduServiceList = new  ArrayList <ApduServiceInfo>();
384                     for (ServiceAidInfo serviceInfo : conflictingServices)
385                         apduServiceList.add(serviceInfo.service);
386                     for (ServiceAidInfo serviceInfo : aidServices)
387                         apduServiceList.add(serviceInfo.service);
388                     resolveinfo.prefixInfo =
389                          findPrefixConflictForSubsetAid(aidServices.get(0).aid ,apduServiceList,false);
390                 }
391                 return resolveinfo;
392             }
393         }
394     }
395 
generateServiceMapLocked(List<ApduServiceInfo> services)396     void generateServiceMapLocked(List<ApduServiceInfo> services) {
397         // Easiest is to just build the entire tree again
398         mAidServices.clear();
399         for (ApduServiceInfo service : services) {
400             if (DBG) Log.d(TAG, "generateServiceMap component: " + service.getComponent());
401             List<String> prefixAids = service.getPrefixAids();
402             List<String> subSetAids = service.getSubsetAids();
403 
404             for (String aid : service.getAids()) {
405                 if (!CardEmulation.isValidAid(aid)) {
406                     Log.e(TAG, "Aid " + aid + " is not valid.");
407                     continue;
408                 }
409                 if (aid.endsWith("*") && !supportsAidPrefixRegistration()) {
410                     Log.e(TAG, "Prefix AID " + aid + " ignored on device that doesn't support it.");
411                     continue;
412                 } else if (supportsAidPrefixRegistration() && prefixAids.size() > 0 && isExact(aid)) {
413                     // Check if we already have an overlapping prefix registered for this AID
414                     boolean foundPrefix = false;
415                     for (String prefixAid : prefixAids) {
416                         String prefix = prefixAid.substring(0, prefixAid.length() - 1);
417                         if (aid.startsWith(prefix)) {
418                             Log.e(TAG, "Ignoring exact AID " + aid + " because prefix AID " + prefixAid +
419                                     " is already registered");
420                             foundPrefix = true;
421                             break;
422                         }
423                     }
424                     if (foundPrefix) {
425                         continue;
426                     }
427                 } else if (aid.endsWith("#") && !supportsAidSubsetRegistration()) {
428                     Log.e(TAG, "Subset AID " + aid + " ignored on device that doesn't support it.");
429                     continue;
430                 } else if (supportsAidSubsetRegistration() && subSetAids.size() > 0 && isExact(aid)) {
431                     // Check if we already have an overlapping subset registered for this AID
432                     boolean foundSubset = false;
433                     for (String subsetAid : subSetAids) {
434                         String plainSubset = subsetAid.substring(0, subsetAid.length() - 1);
435                         if (plainSubset.startsWith(aid)) {
436                             Log.e(TAG, "Ignoring exact AID " + aid + " because subset AID " + plainSubset +
437                                     " is already registered");
438                             foundSubset = true;
439                             break;
440                         }
441                     }
442                     if (foundSubset) {
443                         continue;
444                     }
445                 }
446 
447                 ServiceAidInfo serviceAidInfo = new ServiceAidInfo();
448                 serviceAidInfo.aid = aid.toUpperCase();
449                 serviceAidInfo.service = service;
450                 serviceAidInfo.category = service.getCategoryForAid(aid);
451 
452                 if (mAidServices.containsKey(serviceAidInfo.aid)) {
453                     final ArrayList<ServiceAidInfo> serviceAidInfos =
454                             mAidServices.get(serviceAidInfo.aid);
455                     serviceAidInfos.add(serviceAidInfo);
456                 } else {
457                     final ArrayList<ServiceAidInfo> serviceAidInfos =
458                             new ArrayList<ServiceAidInfo>();
459                     serviceAidInfos.add(serviceAidInfo);
460                     mAidServices.put(serviceAidInfo.aid, serviceAidInfos);
461                 }
462             }
463         }
464     }
465 
isExact(String aid)466     static boolean isExact(String aid) {
467         return (!((aid.endsWith("*") || (aid.endsWith("#")))));
468     }
469 
isPrefix(String aid)470     static boolean isPrefix(String aid) {
471         return aid.endsWith("*");
472     }
473 
isSubset(String aid)474     static boolean isSubset(String aid) {
475         return aid.endsWith("#");
476     }
477 
478     final class ReslovedPrefixConflictAid {
479         String prefixAid = null;
480         boolean matchingSubset = false;
481     }
482 
483     final class AidConflicts {
484         NavigableMap<String, ArrayList<ServiceAidInfo>> conflictMap;
485         final ArrayList<ServiceAidInfo> services = new ArrayList<ServiceAidInfo>();
486         final HashSet<String> aids = new HashSet<String>();
487     }
488 
findPrefixConflictForSubsetAid(String subsetAid , ArrayList<ApduServiceInfo> prefixServices, boolean priorityRootAid)489     ReslovedPrefixConflictAid findPrefixConflictForSubsetAid(String subsetAid ,
490             ArrayList<ApduServiceInfo> prefixServices, boolean priorityRootAid){
491         ArrayList<String> prefixAids = new ArrayList<String>();
492         String minPrefix = null;
493         //This functions checks whether there is a prefix AID matching to subset AID
494         //Because both the subset AID and matching smaller perfix are to be added to routing table.
495         //1.Finds the prefix matching AID in the services sent.
496         //2.Find the smallest prefix among matching prefix and add it only if it is not same as susbet AID.
497         //3..If the subset AID and prefix AID are same add only one AID with both prefix , subset bits set.
498         // Cut off "#"
499         String plainSubsetAid = subsetAid.substring(0, subsetAid.length() - 1);
500         for (ApduServiceInfo service : prefixServices) {
501             for (String prefixAid : service.getPrefixAids()) {
502                 // Cut off "#"
503                 String plainPrefix= prefixAid.substring(0, prefixAid.length() - 1);
504                 if( plainSubsetAid.startsWith(plainPrefix)) {
505                     if (priorityRootAid) {
506                        if (CardEmulation.CATEGORY_PAYMENT.equals(service.getCategoryForAid(prefixAid)) ||
507                                (service.getComponent().equals(mPreferredForegroundService)))
508                            prefixAids.add(prefixAid);
509                     } else {
510                         prefixAids.add(prefixAid);
511                     }
512                 }
513             }
514         }
515         if (prefixAids.size() > 0)
516             minPrefix = Collections.min(prefixAids);
517         ReslovedPrefixConflictAid resolvedPrefix = new ReslovedPrefixConflictAid();
518         resolvedPrefix.prefixAid = minPrefix;
519         if ((minPrefix != null ) &&
520                 plainSubsetAid.equalsIgnoreCase(minPrefix.substring(0, minPrefix.length() - 1)))
521             resolvedPrefix.matchingSubset = true;
522         return resolvedPrefix;
523     }
524 
findConflictsForPrefixLocked(String prefixAid)525     AidConflicts findConflictsForPrefixLocked(String prefixAid) {
526         AidConflicts prefixConflicts = new AidConflicts();
527         String plainAid = prefixAid.substring(0, prefixAid.length() - 1); // Cut off "*"
528         String lastAidWithPrefix = String.format("%-32s", plainAid).replace(' ', 'F');
529         if (DBG) Log.d(TAG, "Finding AIDs in range [" + plainAid + " - " +
530                 lastAidWithPrefix + "]");
531         prefixConflicts.conflictMap =
532                 mAidServices.subMap(plainAid, true, lastAidWithPrefix, true);
533         for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
534                 prefixConflicts.conflictMap.entrySet()) {
535             if (!entry.getKey().equalsIgnoreCase(prefixAid)) {
536                 if (DBG)
537                     Log.d(TAG, "AID " + entry.getKey() + " conflicts with prefix; " +
538                             " adding handling services for conflict resolution.");
539                 prefixConflicts.services.addAll(entry.getValue());
540                 prefixConflicts.aids.add(entry.getKey());
541             }
542         }
543         return prefixConflicts;
544     }
545 
findConflictsForSubsetAidLocked(String subsetAid)546     AidConflicts findConflictsForSubsetAidLocked(String subsetAid) {
547         AidConflicts subsetConflicts = new AidConflicts();
548         // Cut off "@"
549         String lastPlainAid = subsetAid.substring(0, subsetAid.length() - 1);
550         // Cut off "@"
551         String plainSubsetAid = subsetAid.substring(0, subsetAid.length() - 1);
552         String firstAid = subsetAid.substring(0, 10);
553         if (DBG) Log.d(TAG, "Finding AIDs in range [" + firstAid + " - " +
554             lastPlainAid + "]");
555         subsetConflicts.conflictMap = new TreeMap();
556         for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
557             mAidServices.entrySet()) {
558             String aid = entry.getKey();
559             String plainAid = aid;
560             if (isSubset(aid) || isPrefix(aid))
561                 plainAid = aid.substring(0, aid.length() - 1);
562             if (plainSubsetAid.startsWith(plainAid))
563                 subsetConflicts.conflictMap.put(entry.getKey(),entry.getValue());
564         }
565         for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
566             subsetConflicts.conflictMap.entrySet()) {
567             if (!entry.getKey().equalsIgnoreCase(subsetAid)) {
568                 if (DBG)
569                     Log.d(TAG, "AID " + entry.getKey() + " conflicts with subset AID; " +
570                             " adding handling services for conflict resolution.");
571                 subsetConflicts.services.addAll(entry.getValue());
572                 subsetConflicts.aids.add(entry.getKey());
573             }
574         }
575         return subsetConflicts;
576     }
577 
generateAidCacheLocked()578     void generateAidCacheLocked() {
579         mAidCache.clear();
580         // Get all exact and prefix AIDs in an ordered list
581         final TreeMap<String, AidResolveInfo> aidCache = new TreeMap<String, AidResolveInfo>();
582 
583         //aidCache is temproary cache for geenrating the first prefix based lookup table.
584         PriorityQueue<String> aidsToResolve = new PriorityQueue<String>(mAidServices.keySet());
585         aidCache.clear();
586         while (!aidsToResolve.isEmpty()) {
587             final ArrayList<String> resolvedAids = new ArrayList<String>();
588 
589             String aidToResolve = aidsToResolve.peek();
590             // Because of the lexicographical ordering, all following AIDs either start with the
591             // same bytes and are longer, or start with different bytes.
592 
593             // A special case is if another service registered the same AID as a prefix, in
594             // which case we want to start with that AID, since it conflicts with this one
595             // All exact and suffix and prefix AID must be checked for conflicting cases
596             if (aidsToResolve.contains(aidToResolve + "*")) {
597                 aidToResolve = aidToResolve + "*";
598             }
599             if (DBG) Log.d(TAG, "generateAidCacheLocked: starting with aid " + aidToResolve);
600 
601             if (isPrefix(aidToResolve)) {
602                 // This AID itself is a prefix; let's consider this prefix as the "root",
603                 // and all conflicting AIDs as its children.
604                 // For example, if "A000000003*" is the prefix root,
605                 // "A000000003", "A00000000301*", "A0000000030102" are all conflicting children AIDs
606                 final ArrayList<ServiceAidInfo> prefixServices = new ArrayList<ServiceAidInfo>(
607                         mAidServices.get(aidToResolve));
608 
609                 // Find all conflicting children services
610                 AidConflicts prefixConflicts = findConflictsForPrefixLocked(aidToResolve);
611 
612                 // Resolve conflicts
613                 AidResolveInfo resolveInfo = resolveAidConflictLocked(prefixServices,
614                         prefixConflicts.services);
615                 aidCache.put(aidToResolve, resolveInfo);
616                 resolvedAids.add(aidToResolve);
617                 if (resolveInfo.defaultService != null) {
618                     // This prefix is the default; therefore, AIDs of all conflicting children
619                     // will no longer be evaluated.
620                     resolvedAids.addAll(prefixConflicts.aids);
621                     for (String aid : resolveInfo.defaultService.getSubsetAids()) {
622                         if (prefixConflicts.aids.contains(aid)) {
623                             if ((CardEmulation.CATEGORY_PAYMENT.equals(resolveInfo.defaultService.getCategoryForAid(aid))) ||
624                                     (resolveInfo.defaultService.getComponent().equals(mPreferredForegroundService))) {
625                                 AidResolveInfo childResolveInfo = resolveAidConflictLocked(mAidServices.get(aid), false);
626                                 aidCache.put(aid,childResolveInfo);
627                                 Log.d(TAG, "AID " + aid+ " shared with prefix; " +
628                                                 "adding subset .");
629                              }
630                         }
631                    }
632                 } else if (resolveInfo.services.size() > 0) {
633                     // This means we don't have a default for this prefix and all its
634                     // conflicting children. So, for all conflicting AIDs, just add
635                     // all handling services without setting a default
636                     boolean foundChildService = false;
637                     for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
638                             prefixConflicts.conflictMap.entrySet()) {
639                         if (!entry.getKey().equalsIgnoreCase(aidToResolve)) {
640                             if (DBG)
641                                 Log.d(TAG, "AID " + entry.getKey() + " shared with prefix; " +
642                                         " adding all handling services.");
643                             AidResolveInfo childResolveInfo = resolveAidConflictLocked(
644                                     entry.getValue(), false);
645                             // Special case: in this case all children AIDs must be routed to the
646                             // host, so we can ask the user which service is preferred.
647                             // Since these are all "children" of the prefix, they don't need
648                             // to be routed, since the prefix will already get routed to the host
649                             childResolveInfo.mustRoute = false;
650                             aidCache.put(entry.getKey(),childResolveInfo);
651                             resolvedAids.add(entry.getKey());
652                             foundChildService |= !childResolveInfo.services.isEmpty();
653                         }
654                     }
655                     // Special case: if in the end we didn't add any children services,
656                     // and the prefix has only one service, make that default
657                     if (!foundChildService && resolveInfo.services.size() == 1) {
658                         resolveInfo.defaultService = resolveInfo.services.get(0);
659                     }
660                 } else {
661                     // This prefix is not handled at all; we will evaluate
662                     // the children separately in next passes.
663                 }
664             } else {
665                 // Exact AID and no other conflicting AID registrations present
666                 // This is true because aidsToResolve is lexicographically ordered, and
667                 // so by necessity all other AIDs are different than this AID or longer.
668                 if (DBG) Log.d(TAG, "Exact AID, resolving.");
669                 final ArrayList<ServiceAidInfo> conflictingServiceInfos =
670                         new ArrayList<ServiceAidInfo>(mAidServices.get(aidToResolve));
671                 aidCache.put(aidToResolve, resolveAidConflictLocked(conflictingServiceInfos, true));
672                 resolvedAids.add(aidToResolve);
673             }
674 
675             // Remove the AIDs we resolved from the list of AIDs to resolve
676             if (DBG) Log.d(TAG, "AIDs: " + resolvedAids + " were resolved.");
677             aidsToResolve.removeAll(resolvedAids);
678             resolvedAids.clear();
679         }
680         PriorityQueue<String> reversedQueue = new PriorityQueue<String>(1, Collections.reverseOrder());
681         reversedQueue.addAll(aidCache.keySet());
682         while (!reversedQueue.isEmpty()) {
683             final ArrayList<String> resolvedAids = new ArrayList<String>();
684 
685             String aidToResolve = reversedQueue.peek();
686             if (isPrefix(aidToResolve)) {
687                 String matchingSubset = aidToResolve.substring(0,aidToResolve.length()-1 ) + "#";
688                 if (DBG) Log.d(TAG, "matching subset"+matchingSubset);
689                 if (reversedQueue.contains(matchingSubset))
690                      aidToResolve = aidToResolve.substring(0,aidToResolve.length()-1) + "#";
691             }
692             if (isSubset(aidToResolve)) {
693                 if (DBG) Log.d(TAG, "subset resolving aidToResolve  "+aidToResolve);
694                 final ArrayList<ServiceAidInfo> subsetServices = new ArrayList<ServiceAidInfo>(
695                         mAidServices.get(aidToResolve));
696 
697                 // Find all conflicting children services
698                 AidConflicts aidConflicts = findConflictsForSubsetAidLocked(aidToResolve);
699 
700                 // Resolve conflicts
701                 AidResolveInfo resolveInfo = resolveAidConflictLocked(subsetServices,
702                         aidConflicts.services);
703                 mAidCache.put(aidToResolve, resolveInfo);
704                 resolvedAids.add(aidToResolve);
705                 if (resolveInfo.defaultService != null) {
706                     // This subset is the default; therefore, AIDs of all conflicting children
707                     // will no longer be evaluated.Check for any prefix matching in the same service
708                     if (resolveInfo.prefixInfo != null && resolveInfo.prefixInfo.prefixAid != null &&
709                             !resolveInfo.prefixInfo.matchingSubset) {
710                         if (DBG)
711                             Log.d(TAG, "AID default " + resolveInfo.prefixInfo.prefixAid +
712                                     " prefix AID shared with dsubset root; " +
713                                     " adding prefix aid");
714                         AidResolveInfo childResolveInfo = resolveAidConflictLocked(
715                         mAidServices.get(resolveInfo.prefixInfo.prefixAid), false);
716                         mAidCache.put(resolveInfo.prefixInfo.prefixAid, childResolveInfo);
717                     }
718                     resolvedAids.addAll(aidConflicts.aids);
719                 } else if (resolveInfo.services.size() > 0) {
720                     // This means we don't have a default for this subset and all its
721                     // conflicting children. So, for all conflicting AIDs, just add
722                     // all handling services without setting a default
723                     boolean foundChildService = false;
724                     for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
725                         aidConflicts.conflictMap.entrySet()) {
726                         // We need to add shortest prefix among them.
727                         if (!entry.getKey().equalsIgnoreCase(aidToResolve)) {
728                             if (DBG)
729                                 Log.d(TAG, "AID " + entry.getKey() + " shared with subset root; " +
730                                         " adding all handling services.");
731                             AidResolveInfo childResolveInfo = resolveAidConflictLocked(
732                                 entry.getValue(), false);
733                             // Special case: in this case all children AIDs must be routed to the
734                             // host, so we can ask the user which service is preferred.
735                             // Since these are all "children" of the subset, they don't need
736                             // to be routed, since the subset will already get routed to the host
737                             childResolveInfo.mustRoute = false;
738                             mAidCache.put(entry.getKey(),childResolveInfo);
739                             resolvedAids.add(entry.getKey());
740                             foundChildService |= !childResolveInfo.services.isEmpty();
741                         }
742                     }
743                     if(resolveInfo.prefixInfo != null &&
744                             resolveInfo.prefixInfo.prefixAid != null &&
745                             !resolveInfo.prefixInfo.matchingSubset) {
746                         AidResolveInfo childResolveInfo = resolveAidConflictLocked(
747                         mAidServices.get(resolveInfo.prefixInfo.prefixAid), false);
748                         mAidCache.put(resolveInfo.prefixInfo.prefixAid, childResolveInfo);
749                         if (DBG)
750                             Log.d(TAG, "AID " + resolveInfo.prefixInfo.prefixAid +
751                                     " prefix AID shared with subset root; " +
752                                     " adding prefix aid");
753                     }
754                     // Special case: if in the end we didn't add any children services,
755                     // and the subset has only one service, make that default
756                     if (!foundChildService && resolveInfo.services.size() == 1) {
757                         resolveInfo.defaultService = resolveInfo.services.get(0);
758                     }
759                 } else {
760                     // This subset is not handled at all; we will evaluate
761                     // the children separately in next passes.
762                 }
763             } else {
764                 // Exact AID and no other conflicting AID registrations present. This is
765                 // true because reversedQueue is lexicographically ordered in revrese, and
766                 // so by necessity all other AIDs are different than this AID or shorter.
767                 if (DBG) Log.d(TAG, "Exact or Prefix AID."+aidToResolve);
768                 mAidCache.put(aidToResolve, aidCache.get(aidToResolve));
769                 resolvedAids.add(aidToResolve);
770             }
771 
772             // Remove the AIDs we resolved from the list of AIDs to resolve
773             if (DBG) Log.d(TAG, "AIDs: " + resolvedAids + " were resolved.");
774             reversedQueue.removeAll(resolvedAids);
775             resolvedAids.clear();
776         }
777 
778         updateRoutingLocked(false);
779     }
780 
updateRoutingLocked(boolean force)781     void updateRoutingLocked(boolean force) {
782         if (!mNfcEnabled) {
783             if (DBG) Log.d(TAG, "Not updating routing table because NFC is off.");
784             return;
785         }
786         final HashMap<String, AidRoutingManager.AidEntry> routingEntries = Maps.newHashMap();
787         // For each AID, find interested services
788         for (Map.Entry<String, AidResolveInfo> aidEntry:
789                 mAidCache.entrySet()) {
790             String aid = aidEntry.getKey();
791             AidResolveInfo resolveInfo = aidEntry.getValue();
792             if (!resolveInfo.mustRoute) {
793                 if (DBG) Log.d(TAG, "Not routing AID " + aid + " on request.");
794                 continue;
795             }
796             AidRoutingManager.AidEntry aidType = mRoutingManager.new AidEntry();
797             if (aid.endsWith("#")) {
798                 aidType.aidInfo |= AID_ROUTE_QUAL_SUBSET;
799             }
800             if(aid.endsWith("*") || (resolveInfo.prefixInfo != null &&
801                     resolveInfo.prefixInfo.matchingSubset)) {
802                 aidType.aidInfo |= AID_ROUTE_QUAL_PREFIX;
803             }
804             if (resolveInfo.services.size() == 0) {
805                 // No interested services
806             } else if (resolveInfo.defaultService != null) {
807                 // There is a default service set, route to where that service resides -
808                 // either on the host (HCE) or on an SE.
809                 aidType.isOnHost = resolveInfo.defaultService.isOnHost();
810                 if (!aidType.isOnHost) {
811                     aidType.offHostSE =
812                             resolveInfo.defaultService.getOffHostSecureElement();
813                 }
814                 routingEntries.put(aid, aidType);
815             } else if (resolveInfo.services.size() == 1) {
816                 // Only one service, but not the default, must route to host
817                 // to ask the user to choose one.
818                 if (resolveInfo.category.equals(
819                         CardEmulation.CATEGORY_PAYMENT)) {
820                     aidType.isOnHost = true;
821                 } else {
822                     aidType.isOnHost = resolveInfo.services.get(0).isOnHost();
823                     if (!aidType.isOnHost) {
824                         aidType.offHostSE =
825                                 resolveInfo.services.get(0).getOffHostSecureElement();
826                     }
827                 }
828                 routingEntries.put(aid, aidType);
829             } else if (resolveInfo.services.size() > 1) {
830                 // Multiple services if all the services are routing to same
831                 // offhost then the service should be routed to off host.
832                 boolean onHost = false;
833                 String offHostSE = null;
834                 for (ApduServiceInfo service : resolveInfo.services) {
835                     // In case there is at least one service which routes to host
836                     // Route it to host for user to select which service to use
837                     onHost |= service.isOnHost();
838                     if (!onHost) {
839                         if (offHostSE == null) {
840                             offHostSE = service.getOffHostSecureElement();
841                         } else if (!offHostSE.equals(
842                                 service.getOffHostSecureElement())) {
843                             // There are registerations to different SEs, route this
844                             // to host and have user choose a service for this AID
845                             offHostSE = null;
846                             onHost = true;
847                             break;
848                         }
849                     }
850                 }
851                 aidType.isOnHost = onHost;
852                 aidType.offHostSE = onHost ? null : offHostSE;
853                 routingEntries.put(aid, aidType);
854             }
855         }
856         mRoutingManager.configureRouting(routingEntries, force);
857     }
858 
onServicesUpdated(int userId, List<ApduServiceInfo> services)859     public void onServicesUpdated(int userId, List<ApduServiceInfo> services) {
860         if (DBG) Log.d(TAG, "onServicesUpdated");
861         synchronized (mLock) {
862             if (ActivityManager.getCurrentUser() == userId) {
863                 // Rebuild our internal data-structures
864                 generateServiceMapLocked(services);
865                 generateAidCacheLocked();
866             } else {
867                 if (DBG) Log.d(TAG, "Ignoring update because it's not for the current user.");
868             }
869         }
870     }
871 
onPreferredPaymentServiceChanged(ComponentName service)872     public void onPreferredPaymentServiceChanged(ComponentName service) {
873         if (DBG) Log.d(TAG, "Preferred payment service changed.");
874        synchronized (mLock) {
875            mPreferredPaymentService = service;
876            generateAidCacheLocked();
877        }
878     }
879 
onPreferredForegroundServiceChanged(ComponentName service)880     public void onPreferredForegroundServiceChanged(ComponentName service) {
881         if (DBG) Log.d(TAG, "Preferred foreground service changed.");
882         synchronized (mLock) {
883             mPreferredForegroundService = service;
884             generateAidCacheLocked();
885         }
886     }
887 
getPreferredService()888     public ComponentName getPreferredService() {
889         if (mPreferredForegroundService != null) {
890             // return current foreground service
891             return mPreferredForegroundService;
892         } else {
893             // return current preferred service
894             return mPreferredPaymentService;
895         }
896     }
897 
onNfcDisabled()898     public void onNfcDisabled() {
899         synchronized (mLock) {
900             mNfcEnabled = false;
901         }
902         mRoutingManager.onNfccRoutingTableCleared();
903     }
904 
onNfcEnabled()905     public void onNfcEnabled() {
906         synchronized (mLock) {
907             mNfcEnabled = true;
908             updateRoutingLocked(false);
909         }
910     }
911 
onSecureNfcToggled()912     public void onSecureNfcToggled() {
913         synchronized (mLock) {
914             updateRoutingLocked(true);
915         }
916     }
917 
dumpEntry(Map.Entry<String, AidResolveInfo> entry)918     String dumpEntry(Map.Entry<String, AidResolveInfo> entry) {
919         StringBuilder sb = new StringBuilder();
920         String category = entry.getValue().category;
921         ApduServiceInfo defaultServiceInfo = entry.getValue().defaultService;
922         sb.append("    \"" + entry.getKey() + "\" (category: " + category + ")\n");
923         ComponentName defaultComponent = defaultServiceInfo != null ?
924                 defaultServiceInfo.getComponent() : null;
925 
926         for (ApduServiceInfo serviceInfo : entry.getValue().services) {
927             sb.append("        ");
928             if (serviceInfo.getComponent().equals(defaultComponent)) {
929                 sb.append("*DEFAULT* ");
930             }
931             sb.append(serviceInfo.getComponent() +
932                     " (Description: " + serviceInfo.getDescription() + ")\n");
933         }
934         return sb.toString();
935     }
936 
dump(FileDescriptor fd, PrintWriter pw, String[] args)937     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
938         pw.println("    AID cache entries: ");
939         for (Map.Entry<String, AidResolveInfo> entry : mAidCache.entrySet()) {
940             pw.println(dumpEntry(entry));
941         }
942         pw.println("    Service preferred by foreground app: " + mPreferredForegroundService);
943         pw.println("    Preferred payment service: " + mPreferredPaymentService);
944         pw.println("");
945         mRoutingManager.dump(fd, pw, args);
946         pw.println("");
947     }
948 }
949