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