RegisteredAidCache.java revision 5b60927762b8512622e5a1d53b93a4b3b49acba2
1package com.android.nfc.cardemulation; 2 3import android.app.ActivityManager; 4import android.content.ComponentName; 5import android.content.Context; 6import android.content.Intent; 7import android.database.ContentObserver; 8import android.net.Uri; 9import android.nfc.cardemulation.ApduServiceInfo; 10import android.nfc.cardemulation.CardEmulation; 11import android.nfc.cardemulation.ApduServiceInfo.AidGroup; 12import android.os.Handler; 13import android.os.Looper; 14import android.os.UserHandle; 15import android.provider.Settings; 16import android.util.Log; 17 18import com.google.android.collect.Maps; 19 20import java.util.ArrayList; 21import java.util.HashMap; 22import java.util.HashSet; 23import java.util.List; 24import java.util.Map; 25import java.util.Set; 26import java.util.SortedMap; 27import java.util.TreeMap; 28 29public class RegisteredAidCache implements RegisteredServicesCache.Callback { 30 static final String TAG = "RegisteredAidCache"; 31 32 static final boolean DBG = true; 33 34 // mAidServices is a tree that maps an AID to a list of handling services 35 // on Android. It is only valid for the current user. 36 final TreeMap<String, ArrayList<ApduServiceInfo>> mAidToServices = 37 new TreeMap<String, ArrayList<ApduServiceInfo>>(); 38 39 // mAidCache is a lookup table for quickly mapping an AID to one or 40 // more services. It differs from mAidServices in the sense that it 41 // has already accounted for defaults, and hence its return value 42 // is authoritative for the current set of services and defaults. 43 // It is only valid for the current user. 44 final HashMap<String, AidResolveInfo> mAidCache = 45 Maps.newHashMap(); 46 47 final HashMap<String, ComponentName> mCategoryDefaults = 48 Maps.newHashMap(); 49 50 final class AidResolveInfo { 51 List<ApduServiceInfo> services; 52 ApduServiceInfo defaultService; 53 String aid; 54 } 55 56 /** 57 * AIDs per category 58 */ 59 public final HashMap<String, Set<String>> mCategoryAids = 60 Maps.newHashMap(); 61 62 final Handler mHandler = new Handler(Looper.getMainLooper()); 63 final RegisteredServicesCache mServiceCache; 64 65 final Object mLock = new Object(); 66 final Context mContext; 67 final AidRoutingManager mRoutingManager; 68 final SettingsObserver mSettingsObserver; 69 70 ComponentName mNextTapComponent = null; 71 boolean mNfcEnabled = false; 72 73 private final class SettingsObserver extends ContentObserver { 74 public SettingsObserver(Handler handler) { 75 super(handler); 76 } 77 78 @Override 79 public void onChange(boolean selfChange, Uri uri) { 80 super.onChange(selfChange, uri); 81 synchronized (mLock) { 82 // Do it just for the current user. If it was in fact 83 // a change made for another user, we'll sync it down 84 // on user switch. 85 int currentUser = ActivityManager.getCurrentUser(); 86 boolean changed = updateFromSettingsLocked(currentUser); 87 if (changed) { 88 generateAidCacheLocked(); 89 updateRoutingLocked(); 90 } else { 91 if (DBG) Log.d(TAG, "Not updating aid cache + routing: nothing changed."); 92 } 93 } 94 } 95 }; 96 97 public RegisteredAidCache(Context context, AidRoutingManager routingManager) { 98 mSettingsObserver = new SettingsObserver(mHandler); 99 mContext = context; 100 mServiceCache = new RegisteredServicesCache(context, this); 101 mRoutingManager = routingManager; 102 103 updateFromSettingsLocked(ActivityManager.getCurrentUser()); 104 } 105 106 public boolean isNextTapOverriden() { 107 synchronized (mLock) { 108 return mNextTapComponent != null; 109 } 110 } 111 112 public AidResolveInfo resolveAidPrefix(String aid) { 113 synchronized (mLock) { 114 char nextAidChar = (char) (aid.charAt(aid.length() - 1) + 1); 115 String nextAid = aid.substring(0, aid.length() - 1) + nextAidChar; 116 SortedMap<String, ArrayList<ApduServiceInfo>> matches = 117 mAidToServices.subMap(aid, nextAid); 118 // The first match is lexicographically closest to what the reader asked; 119 if (matches.isEmpty()) { 120 return null; 121 } else { 122 AidResolveInfo resolveInfo = mAidCache.get(matches.firstKey()); 123 // Let the caller know which AID got selected 124 resolveInfo.aid = matches.firstKey(); 125 return resolveInfo; 126 } 127 } 128 } 129 130 public String getCategoryForAid(String aid) { 131 synchronized (mLock) { 132 Set<String> paymentAids = mCategoryAids.get(CardEmulation.CATEGORY_PAYMENT); 133 if (paymentAids != null && paymentAids.contains(aid)) { 134 return CardEmulation.CATEGORY_PAYMENT; 135 } else { 136 return CardEmulation.CATEGORY_OTHER; 137 } 138 } 139 } 140 141 public boolean isDefaultServiceForAid(int userId, ComponentName service, String aid) { 142 AidResolveInfo resolveInfo = null; 143 boolean serviceFound = false; 144 synchronized (mLock) { 145 serviceFound = mServiceCache.hasService(userId, service); 146 } 147 if (!serviceFound) { 148 // If we don't know about this service yet, it may have just been enabled 149 // using PackageManager.setComponentEnabledSetting(). The PackageManager 150 // broadcasts are delayed by 10 seconds in that scenario, which causes 151 // calls to our APIs referencing that service to fail. 152 // Hence, update the cache in case we don't know about the service. 153 if (DBG) Log.d(TAG, "Didn't find passed in service, invalidating cache."); 154 mServiceCache.invalidateCache(userId); 155 } 156 synchronized (mLock) { 157 resolveInfo = mAidCache.get(aid); 158 } 159 if (resolveInfo.services == null || resolveInfo.services.size() == 0) return false; 160 161 if (resolveInfo.defaultService != null) { 162 return service.equals(resolveInfo.defaultService.getComponent()); 163 } else if (resolveInfo.services.size() == 1) { 164 return service.equals(resolveInfo.services.get(0).getComponent()); 165 } else { 166 // More than one service, not the default 167 return false; 168 } 169 } 170 171 public boolean setDefaultServiceForCategory(int userId, ComponentName service, 172 String category) { 173 if (!CardEmulation.CATEGORY_PAYMENT.equals(category)) { 174 Log.e(TAG, "Not allowing defaults for category " + category); 175 return false; 176 } 177 synchronized (mLock) { 178 // TODO Not really nice to be writing to Settings.Secure here... 179 // ideally we overlay our local changes over whatever is in 180 // Settings.Secure 181 if (service == null || mServiceCache.hasService(userId, service)) { 182 Settings.Secure.putStringForUser(mContext.getContentResolver(), 183 Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, 184 service != null ? service.flattenToString() : null, userId); 185 } else { 186 Log.e(TAG, "Could not find default service to make default: " + service); 187 } 188 } 189 return true; 190 } 191 192 public boolean isDefaultServiceForCategory(int userId, String category, 193 ComponentName service) { 194 boolean serviceFound = false; 195 synchronized (mLock) { 196 // If we don't know about this service yet, it may have just been enabled 197 // using PackageManager.setComponentEnabledSetting(). The PackageManager 198 // broadcasts are delayed by 10 seconds in that scenario, which causes 199 // calls to our APIs referencing that service to fail. 200 // Hence, update the cache in case we don't know about the service. 201 serviceFound = mServiceCache.hasService(userId, service); 202 } 203 if (!serviceFound) { 204 if (DBG) Log.d(TAG, "Didn't find passed in service, invalidating cache."); 205 mServiceCache.invalidateCache(userId); 206 } 207 ComponentName defaultService = 208 getDefaultServiceForCategory(userId, category, true); 209 return (defaultService != null && defaultService.equals(service)); 210 } 211 212 ComponentName getDefaultServiceForCategory(int userId, String category, 213 boolean validateInstalled) { 214 if (!CardEmulation.CATEGORY_PAYMENT.equals(category)) { 215 Log.e(TAG, "Not allowing defaults for category " + category); 216 return null; 217 } 218 synchronized (mLock) { 219 // Load current payment default from settings 220 String name = Settings.Secure.getStringForUser( 221 mContext.getContentResolver(), Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, 222 userId); 223 if (name != null) { 224 ComponentName service = ComponentName.unflattenFromString(name); 225 if (!validateInstalled || service == null) { 226 return service; 227 } else { 228 return mServiceCache.hasService(userId, service) ? service : null; 229 } 230 } else { 231 return null; 232 } 233 } 234 } 235 236 public List<ApduServiceInfo> getServicesForCategory(int userId, String category) { 237 return mServiceCache.getServicesForCategory(userId, category); 238 } 239 240 public boolean setDefaultForNextTap(int userId, ComponentName service) { 241 synchronized (mLock) { 242 if (service != null) { 243 mNextTapComponent = service; 244 } else { 245 mNextTapComponent = null; 246 } 247 // Update cache and routing table 248 generateAidCacheLocked(); 249 updateRoutingLocked(); 250 } 251 return true; 252 } 253 254 /** 255 * Resolves an AID to a set of services that can handle it. 256 */ 257 AidResolveInfo resolveAidLocked(List<ApduServiceInfo> resolvedServices, String aid) { 258 if (resolvedServices == null || resolvedServices.size() == 0) { 259 if (DBG) Log.d(TAG, "Could not resolve AID " + aid + " to any service."); 260 return null; 261 } 262 AidResolveInfo resolveInfo = new AidResolveInfo(); 263 Log.e(TAG, "resolveAidLocked: resolving AID " + aid); 264 resolveInfo.services = new ArrayList<ApduServiceInfo>(); 265 resolveInfo.services.addAll(resolvedServices); 266 resolveInfo.defaultService = null; 267 268 ComponentName defaultComponent = mNextTapComponent; 269 if (DBG) Log.d(TAG, "resolveAidLocked: next tap component is " + defaultComponent); 270 Set<String> paymentAids = mCategoryAids.get(CardEmulation.CATEGORY_PAYMENT); 271 if (paymentAids != null && paymentAids.contains(aid)) { 272 if (DBG) Log.d(TAG, "resolveAidLocked: AID " + aid + " is a payment AID"); 273 // This AID has been registered as a payment AID by at least one service. 274 // Get default component for payment if no next tap default. 275 if (defaultComponent == null) { 276 defaultComponent = mCategoryDefaults.get(CardEmulation.CATEGORY_PAYMENT); 277 } 278 if (DBG) Log.d(TAG, "resolveAidLocked: default payment component is " 279 + defaultComponent); 280 if (resolvedServices.size() == 1) { 281 ApduServiceInfo resolvedService = resolvedServices.get(0); 282 Log.d(TAG, "resolveAidLocked: resolved single service " + 283 resolvedService.getComponent()); 284 if (defaultComponent != null && 285 defaultComponent.equals(resolvedService.getComponent())) { 286 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to (default) " + 287 resolvedService.getComponent()); 288 resolveInfo.defaultService = resolvedService; 289 } else { 290 // So..since we resolved to only one service, and this AID 291 // is a payment AID, we know that this service is the only 292 // service that has registered for this AID and in fact claimed 293 // it was a payment AID. 294 // There's two cases: 295 // 1. All other AIDs in the payment group are uncontended: 296 // in this case, just route to this app. It won't get 297 // in the way of other apps, and is likely to interact 298 // with different terminal infrastructure anyway. 299 // 2. At least one AID in the payment group is contended: 300 // in this case, we should ask the user to confirm, 301 // since it is likely to contend with other apps, even 302 // when touching the same terminal. 303 boolean foundConflict = false; 304 for (AidGroup aidGroup : resolvedService.getAidGroups()) { 305 if (aidGroup.getCategory().equals(CardEmulation.CATEGORY_PAYMENT)) { 306 for (String registeredAid : aidGroup.getAids()) { 307 ArrayList<ApduServiceInfo> servicesForAid = 308 mAidToServices.get(registeredAid); 309 if (servicesForAid != null && servicesForAid.size() > 1) { 310 foundConflict = true; 311 } 312 } 313 } 314 } 315 if (!foundConflict) { 316 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to " + 317 resolvedService.getComponent()); 318 // Treat this as if it's the default for this AID 319 resolveInfo.defaultService = resolvedService; 320 } else { 321 // Allow this service to handle, but don't set as default 322 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing AID " + aid + 323 " to " + resolvedService.getComponent() + 324 ", but will ask confirmation because its AID group is contended."); 325 } 326 } 327 } else if (resolvedServices.size() > 1) { 328 // More services have registered. If there's a default and it 329 // registered this AID, go with the default. Otherwise, add all. 330 if (DBG) Log.d(TAG, "resolveAidLocked: multiple services matched."); 331 if (defaultComponent != null) { 332 for (ApduServiceInfo service : resolvedServices) { 333 if (service.getComponent().equals(defaultComponent)) { 334 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to (default) " 335 + service.getComponent()); 336 resolveInfo.defaultService = service; 337 break; 338 } 339 } 340 if (resolveInfo.defaultService == null) { 341 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to all services"); 342 } 343 } 344 } // else -> should not hit, we checked for 0 before. 345 } else { 346 // This AID is not a payment AID, just return all components 347 // that can handle it, but be mindful of (next tap) defaults. 348 for (ApduServiceInfo service : resolvedServices) { 349 if (service.getComponent().equals(defaultComponent)) { 350 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, " + 351 "routing to (default) " + service.getComponent()); 352 resolveInfo.defaultService = service; 353 break; 354 } 355 } 356 if (resolveInfo.defaultService == null) { 357 // If we didn't find the default, mark the first as default 358 // if there is only one. 359 if (resolveInfo.services.size() == 1) { 360 resolveInfo.defaultService = resolveInfo.services.get(0); 361 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, " + 362 "routing to (default) " + resolveInfo.defaultService.getComponent()); 363 } else { 364 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, routing all"); 365 } 366 } 367 } 368 return resolveInfo; 369 } 370 371 void generateAidTreeLocked(List<ApduServiceInfo> services) { 372 // Easiest is to just build the entire tree again 373 mAidToServices.clear(); 374 for (ApduServiceInfo service : services) { 375 if (DBG) Log.d(TAG, "generateAidTree component: " + service.getComponent()); 376 for (String aid : service.getAids()) { 377 if (DBG) Log.d(TAG, "generateAidTree AID: " + aid); 378 // Check if a mapping exists for this AID 379 if (mAidToServices.containsKey(aid)) { 380 final ArrayList<ApduServiceInfo> aidServices = mAidToServices.get(aid); 381 aidServices.add(service); 382 } else { 383 final ArrayList<ApduServiceInfo> aidServices = 384 new ArrayList<ApduServiceInfo>(); 385 aidServices.add(service); 386 mAidToServices.put(aid, aidServices); 387 } 388 } 389 } 390 } 391 392 void generateAidCategoriesLocked(List<ApduServiceInfo> services) { 393 // Trash existing mapping 394 mCategoryAids.clear(); 395 396 for (ApduServiceInfo service : services) { 397 ArrayList<AidGroup> aidGroups = service.getAidGroups(); 398 if (aidGroups == null) continue; 399 for (AidGroup aidGroup : aidGroups) { 400 String groupCategory = aidGroup.getCategory(); 401 Set<String> categoryAids = mCategoryAids.get(groupCategory); 402 if (categoryAids == null) { 403 categoryAids = new HashSet<String>(); 404 } 405 categoryAids.addAll(aidGroup.getAids()); 406 mCategoryAids.put(groupCategory, categoryAids); 407 } 408 } 409 } 410 411 boolean updateFromSettingsLocked(int userId) { 412 boolean changed = false; 413 414 // Load current payment default from settings 415 String name = Settings.Secure.getStringForUser( 416 mContext.getContentResolver(), Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, 417 userId); 418 ComponentName newDefault = name != null ? ComponentName.unflattenFromString(name) : null; 419 ComponentName oldDefault = mCategoryDefaults.put(CardEmulation.CATEGORY_PAYMENT, 420 newDefault); 421 changed |= newDefault != oldDefault; 422 if (DBG) Log.d(TAG, "Updating default component to: " + (name != null ? 423 ComponentName.unflattenFromString(name) : "null")); 424 return changed; 425 } 426 427 void generateAidCacheLocked() { 428 mAidCache.clear(); 429 for (Map.Entry<String, ArrayList<ApduServiceInfo>> aidEntry: 430 mAidToServices.entrySet()) { 431 String aid = aidEntry.getKey(); 432 if (!mAidCache.containsKey(aid)) { 433 mAidCache.put(aid, resolveAidLocked(aidEntry.getValue(), aid)); 434 } 435 } 436 } 437 438 void updateRoutingLocked() { 439 if (!mNfcEnabled) { 440 if (DBG) Log.d(TAG, "Not updating routing table because NFC is off."); 441 return; 442 } 443 final Set<String> handledAids = new HashSet<String>(); 444 // For each AID, find interested services 445 for (Map.Entry<String, AidResolveInfo> aidEntry: 446 mAidCache.entrySet()) { 447 String aid = aidEntry.getKey(); 448 AidResolveInfo resolveInfo = aidEntry.getValue(); 449 if (resolveInfo.services.size() == 0) { 450 // No interested services, if there is a current routing remove it 451 mRoutingManager.removeAid(aid); 452 } else if (resolveInfo.defaultService != null) { 453 // There is a default service set, route to that service 454 mRoutingManager.setRouteForAid(aid, resolveInfo.defaultService.isOnHost()); 455 } else if (resolveInfo.services.size() == 1) { 456 // Only one service, but not the default, must route to host 457 // to ask the user to confirm. 458 mRoutingManager.setRouteForAid(aid, true); 459 } else if (resolveInfo.services.size() > 1) { 460 // Multiple services, need to route to host to ask 461 mRoutingManager.setRouteForAid(aid, true); 462 } 463 handledAids.add(aid); 464 } 465 // Now, find AIDs in the routing table that are no longer routed to 466 // and remove them. 467 Set<String> routedAids = mRoutingManager.getRoutedAids(); 468 for (String aid : routedAids) { 469 if (!handledAids.contains(aid)) { 470 if (DBG) Log.d(TAG, "Removing routing for AID " + aid + ", because " + 471 "there are no no interested services."); 472 mRoutingManager.removeAid(aid); 473 } 474 } 475 // And commit the routing 476 mRoutingManager.commitRouting(); 477 } 478 479 void showDefaultRemovedDialog() { 480 Intent intent = new Intent(mContext, DefaultRemovedActivity.class); 481 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 482 mContext.startActivityAsUser(intent, UserHandle.CURRENT); 483 } 484 485 void onPaymentDefaultRemoved(int userId, List<ApduServiceInfo> services) { 486 int numPaymentServices = 0; 487 ComponentName lastFoundPaymentService = null; 488 for (ApduServiceInfo service : services) { 489 if (service.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { 490 numPaymentServices++; 491 lastFoundPaymentService = service.getComponent(); 492 } 493 } 494 if (DBG) Log.d(TAG, "Number of payment services is " + 495 Integer.toString(numPaymentServices)); 496 if (numPaymentServices == 0) { 497 if (DBG) Log.d(TAG, "Default removed, no services left."); 498 // No payment services left, unset default and don't ask the user 499 setDefaultServiceForCategory(userId, null, 500 CardEmulation.CATEGORY_PAYMENT); 501 } else if (numPaymentServices == 1) { 502 // Only one left, automatically make it the default 503 if (DBG) Log.d(TAG, "Default removed, making remaining service default."); 504 setDefaultServiceForCategory(userId, lastFoundPaymentService, 505 CardEmulation.CATEGORY_PAYMENT); 506 } else if (numPaymentServices > 1) { 507 // More than one left, unset default and ask the user if he wants 508 // to set a new one 509 if (DBG) Log.d(TAG, "Default removed, asking user to pick."); 510 setDefaultServiceForCategory(userId, null, 511 CardEmulation.CATEGORY_PAYMENT); 512 showDefaultRemovedDialog(); 513 } 514 } 515 516 void setDefaultIfNeededLocked(int userId, List<ApduServiceInfo> services) { 517 int numPaymentServices = 0; 518 ComponentName lastFoundPaymentService = null; 519 for (ApduServiceInfo service : services) { 520 if (service.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { 521 numPaymentServices++; 522 lastFoundPaymentService = service.getComponent(); 523 } 524 } 525 if (numPaymentServices > 1) { 526 // More than one service left, leave default unset 527 if (DBG) Log.d(TAG, "No default set, more than one service left."); 528 } else if (numPaymentServices == 1) { 529 // Make single found payment service the default 530 if (DBG) Log.d(TAG, "No default set, making single service default."); 531 setDefaultServiceForCategory(userId, lastFoundPaymentService, 532 CardEmulation.CATEGORY_PAYMENT); 533 } else { 534 // No payment services left, leave default at null 535 if (DBG) Log.d(TAG, "No default set, last payment service removed."); 536 } 537 } 538 539 void checkDefaultsLocked(int userId, List<ApduServiceInfo> services) { 540 ComponentName defaultPaymentService = 541 getDefaultServiceForCategory(userId, CardEmulation.CATEGORY_PAYMENT, false); 542 Log.d(TAG, "Current default: " + defaultPaymentService); 543 if (defaultPaymentService != null) { 544 // Validate the default is still installed and handling payment 545 ApduServiceInfo serviceInfo = mServiceCache.getService(userId, defaultPaymentService); 546 if (serviceInfo == null) { 547 Log.e(TAG, "Default payment service unexpectedly removed."); 548 onPaymentDefaultRemoved(userId, services); 549 } else if (!serviceInfo.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { 550 if (DBG) Log.d(TAG, "Default payment service had payment category removed"); 551 onPaymentDefaultRemoved(userId, services); 552 } else { 553 // Default still exists and handles the category, nothing do 554 if (DBG) Log.d(TAG, "Default payment service still ok."); 555 } 556 } else { 557 // A payment service may have been removed, leaving only one; 558 // in that case, automatically set that app as default. 559 setDefaultIfNeededLocked(userId, services); 560 } 561 } 562 563 @Override 564 public void onServicesUpdated(int userId, List<ApduServiceInfo> services) { 565 synchronized (mLock) { 566 if (ActivityManager.getCurrentUser() == userId) { 567 // Rebuild our internal data-structures 568 checkDefaultsLocked(userId, services); 569 generateAidTreeLocked(services); 570 generateAidCategoriesLocked(services); 571 generateAidCacheLocked(); 572 updateRoutingLocked(); 573 } else { 574 if (DBG) Log.d(TAG, "Ignoring update because it's not for the current user."); 575 } 576 } 577 } 578 579 public void invalidateCache(int currentUser) { 580 mServiceCache.invalidateCache(currentUser); 581 } 582 583 public void onNfcDisabled() { 584 synchronized (mLock) { 585 mNfcEnabled = false; 586 } 587 mContext.getContentResolver().unregisterContentObserver(mSettingsObserver); 588 mServiceCache.onNfcDisabled(); 589 mRoutingManager.onNfccRoutingTableCleared(); 590 } 591 592 public void onNfcEnabled() { 593 mContext.getContentResolver().registerContentObserver( 594 Settings.Secure.getUriFor(Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT), 595 true, mSettingsObserver, UserHandle.USER_ALL); 596 synchronized (mLock) { 597 mNfcEnabled = true; 598 updateFromSettingsLocked(ActivityManager.getCurrentUser()); 599 } 600 mServiceCache.onNfcEnabled(); 601 } 602} 603