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