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