| package com.android.nfc.cardemulation; |
| |
| import android.app.ActivityManager; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.database.ContentObserver; |
| import android.net.Uri; |
| import android.nfc.cardemulation.ApduServiceInfo; |
| import android.nfc.cardemulation.CardEmulation; |
| import android.nfc.cardemulation.ApduServiceInfo.AidGroup; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.util.Log; |
| |
| import com.google.android.collect.Maps; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.SortedMap; |
| import java.util.TreeMap; |
| |
| public class RegisteredAidCache implements RegisteredServicesCache.Callback { |
| static final String TAG = "RegisteredAidCache"; |
| |
| static final boolean DBG = true; |
| |
| // mAidServices is a tree that maps an AID to a list of handling services |
| // on Android. It is only valid for the current user. |
| final TreeMap<String, ArrayList<ApduServiceInfo>> mAidToServices = |
| new TreeMap<String, ArrayList<ApduServiceInfo>>(); |
| |
| // mAidCache is a lookup table for quickly mapping an AID to one or |
| // more services. It differs from mAidServices in the sense that it |
| // has already accounted for defaults, and hence its return value |
| // is authoritative for the current set of services and defaults. |
| // It is only valid for the current user. |
| final HashMap<String, AidResolveInfo> mAidCache = |
| Maps.newHashMap(); |
| |
| final HashMap<String, ComponentName> mCategoryDefaults = |
| Maps.newHashMap(); |
| |
| final class AidResolveInfo { |
| List<ApduServiceInfo> services; |
| ApduServiceInfo defaultService; |
| String aid; |
| } |
| |
| /** |
| * AIDs per category |
| */ |
| public final HashMap<String, Set<String>> mCategoryAids = |
| Maps.newHashMap(); |
| |
| final Handler mHandler = new Handler(Looper.getMainLooper()); |
| final RegisteredServicesCache mServiceCache; |
| |
| final Object mLock = new Object(); |
| final Context mContext; |
| final AidRoutingManager mRoutingManager; |
| final SettingsObserver mSettingsObserver; |
| |
| ComponentName mNextTapComponent = null; |
| |
| private final class SettingsObserver extends ContentObserver { |
| public SettingsObserver(Handler handler) { |
| super(handler); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange, Uri uri) { |
| super.onChange(selfChange, uri); |
| synchronized (mLock) { |
| // Do it just for the current user. If it was in fact |
| // a change made for another user, we'll sync it down |
| // on user switch. |
| int currentUser = ActivityManager.getCurrentUser(); |
| boolean changed = updateFromSettingsLocked(currentUser); |
| if (changed) { |
| generateAidCacheLocked(); |
| updateRoutingLocked(); |
| } else { |
| if (DBG) Log.d(TAG, "Not updating aid cache + routing: nothing changed."); |
| } |
| } |
| } |
| }; |
| |
| public RegisteredAidCache(Context context, AidRoutingManager routingManager) { |
| mSettingsObserver = new SettingsObserver(mHandler); |
| mContext = context; |
| mServiceCache = new RegisteredServicesCache(context, this); |
| mRoutingManager = routingManager; |
| |
| updateFromSettingsLocked(ActivityManager.getCurrentUser()); |
| } |
| |
| public boolean isNextTapOverriden() { |
| synchronized (mLock) { |
| return mNextTapComponent != null; |
| } |
| } |
| |
| public AidResolveInfo resolveAidPrefix(String aid) { |
| synchronized (mLock) { |
| char nextAidChar = (char) (aid.charAt(aid.length() - 1) + 1); |
| String nextAid = aid.substring(0, aid.length() - 1) + nextAidChar; |
| SortedMap<String, ArrayList<ApduServiceInfo>> matches = |
| mAidToServices.subMap(aid, nextAid); |
| // The first match is lexicographically closest to what the reader asked; |
| if (matches.isEmpty()) { |
| return null; |
| } else { |
| AidResolveInfo resolveInfo = mAidCache.get(matches.firstKey()); |
| // Let the caller know which AID got selected |
| resolveInfo.aid = matches.firstKey(); |
| return resolveInfo; |
| } |
| } |
| } |
| |
| public String getCategoryForAid(String aid) { |
| synchronized (mLock) { |
| Set<String> paymentAids = mCategoryAids.get(CardEmulation.CATEGORY_PAYMENT); |
| if (paymentAids != null && paymentAids.contains(aid)) { |
| return CardEmulation.CATEGORY_PAYMENT; |
| } else { |
| return CardEmulation.CATEGORY_OTHER; |
| } |
| } |
| } |
| |
| public boolean isDefaultServiceForAid(int userId, ComponentName service, String aid) { |
| AidResolveInfo resolveInfo = mAidCache.get(aid); |
| |
| if (resolveInfo.services == null || resolveInfo.services.size() == 0) return false; |
| |
| if (resolveInfo.defaultService != null) { |
| return service.equals(resolveInfo.defaultService.getComponent()); |
| } else if (resolveInfo.services.size() == 1) { |
| return service.equals(resolveInfo.services.get(0).getComponent()); |
| } else { |
| // More than one service, not the default |
| return false; |
| } |
| } |
| |
| public boolean setDefaultServiceForCategory(int userId, ComponentName service, |
| String category) { |
| if (!CardEmulation.CATEGORY_PAYMENT.equals(category)) { |
| Log.e(TAG, "Not allowing defaults for category " + category); |
| return false; |
| } |
| synchronized (mLock) { |
| // TODO Not really nice to be writing to Settings.Secure here... |
| // ideally we overlay our local changes over whatever is in |
| // Settings.Secure |
| if (service == null || mServiceCache.hasService(userId, service)) { |
| Settings.Secure.putStringForUser(mContext.getContentResolver(), |
| Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, |
| service != null ? service.flattenToString() : null, userId); |
| } else { |
| Log.e(TAG, "Could not find default service to make default: " + service); |
| } |
| } |
| return true; |
| } |
| |
| public ComponentName getDefaultServiceForCategory(int userId, String category, |
| boolean validateInstalled) { |
| if (!CardEmulation.CATEGORY_PAYMENT.equals(category)) { |
| Log.e(TAG, "Not allowing defaults for category " + category); |
| return null; |
| } |
| synchronized (mLock) { |
| // Load current payment default from settings |
| String name = Settings.Secure.getStringForUser( |
| mContext.getContentResolver(), Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, |
| userId); |
| if (name != null) { |
| ComponentName service = ComponentName.unflattenFromString(name); |
| if (!validateInstalled || service == null) { |
| return service; |
| } else { |
| return mServiceCache.hasService(userId, service) ? service : null; |
| } |
| } else { |
| return null; |
| } |
| } |
| } |
| |
| public List<ApduServiceInfo> getServicesForCategory(int userId, String category) { |
| return mServiceCache.getServicesForCategory(userId, category); |
| } |
| |
| public boolean setDefaultForNextTap(int userId, ComponentName service) { |
| synchronized (mLock) { |
| if (service != null) { |
| mNextTapComponent = service; |
| } else { |
| mNextTapComponent = null; |
| } |
| // Update cache and routing table |
| generateAidCacheLocked(); |
| updateRoutingLocked(); |
| } |
| return true; |
| } |
| |
| /** |
| * Resolves an AID to a set of services that can handle it. |
| */ |
| AidResolveInfo resolveAidLocked(List<ApduServiceInfo> resolvedServices, String aid) { |
| if (resolvedServices == null || resolvedServices.size() == 0) { |
| if (DBG) Log.d(TAG, "Could not resolve AID " + aid + " to any service."); |
| return null; |
| } |
| AidResolveInfo resolveInfo = new AidResolveInfo(); |
| Log.e(TAG, "resolveAidLocked: resolving AID " + aid); |
| resolveInfo.services = new ArrayList<ApduServiceInfo>(); |
| resolveInfo.services.addAll(resolvedServices); |
| resolveInfo.defaultService = null; |
| |
| ComponentName defaultComponent = mNextTapComponent; |
| if (DBG) Log.d(TAG, "resolveAidLocked: next tap component is " + defaultComponent); |
| Set<String> paymentAids = mCategoryAids.get(CardEmulation.CATEGORY_PAYMENT); |
| if (paymentAids != null && paymentAids.contains(aid)) { |
| if (DBG) Log.d(TAG, "resolveAidLocked: AID " + aid + " is a payment AID"); |
| // This AID has been registered as a payment AID by at least one service. |
| // Get default component for payment if no next tap default. |
| if (defaultComponent == null) { |
| defaultComponent = mCategoryDefaults.get(CardEmulation.CATEGORY_PAYMENT); |
| } |
| if (DBG) Log.d(TAG, "resolveAidLocked: default payment component is " |
| + defaultComponent); |
| if (resolvedServices.size() == 1) { |
| ApduServiceInfo resolvedService = resolvedServices.get(0); |
| Log.d(TAG, "resolveAidLocked: resolved single service " + |
| resolvedService.getComponent()); |
| if (defaultComponent != null && |
| defaultComponent.equals(resolvedService.getComponent())) { |
| if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to (default) " + |
| resolvedService.getComponent()); |
| resolveInfo.defaultService = resolvedService; |
| } else { |
| // So..since we resolved to only one service, and this AID |
| // is a payment AID, we know that this service is the only |
| // service that has registered for this AID and in fact claimed |
| // it was a payment AID. |
| // There's two cases: |
| // 1. All other AIDs in the payment group are uncontended: |
| // in this case, just route to this app. It won't get |
| // in the way of other apps, and is likely to interact |
| // with different terminal infrastructure anyway. |
| // 2. At least one AID in the payment group is contended: |
| // in this case, we should ask the user to confirm, |
| // since it is likely to contend with other apps, even |
| // when touching the same terminal. |
| boolean foundConflict = false; |
| for (AidGroup aidGroup : resolvedService.getAidGroups()) { |
| if (aidGroup.getCategory().equals(CardEmulation.CATEGORY_PAYMENT)) { |
| for (String registeredAid : aidGroup.getAids()) { |
| ArrayList<ApduServiceInfo> servicesForAid = |
| mAidToServices.get(registeredAid); |
| if (servicesForAid != null && servicesForAid.size() > 1) { |
| foundConflict = true; |
| } |
| } |
| } |
| } |
| if (!foundConflict) { |
| if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to " + |
| resolvedService.getComponent()); |
| // Treat this as if it's the default for this AID |
| resolveInfo.defaultService = resolvedService; |
| } else { |
| // Allow this service to handle, but don't set as default |
| if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing AID " + aid + |
| " to " + resolvedService.getComponent() + |
| ", but will ask confirmation because its AID group is contended."); |
| } |
| } |
| } else if (resolvedServices.size() > 1) { |
| // More services have registered. If there's a default and it |
| // registered this AID, go with the default. Otherwise, add all. |
| if (DBG) Log.d(TAG, "resolveAidLocked: multiple services matched."); |
| if (defaultComponent != null) { |
| for (ApduServiceInfo service : resolvedServices) { |
| if (service.getComponent().equals(defaultComponent)) { |
| if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to (default) " |
| + service.getComponent()); |
| resolveInfo.defaultService = service; |
| break; |
| } |
| } |
| if (resolveInfo.defaultService == null) { |
| if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to all services"); |
| } |
| } |
| } // else -> should not hit, we checked for 0 before. |
| } else { |
| // This AID is not a payment AID, just return all components |
| // that can handle it, but be mindful of (next tap) defaults. |
| for (ApduServiceInfo service : resolvedServices) { |
| if (service.getComponent().equals(defaultComponent)) { |
| if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, " + |
| "routing to (default) " + service.getComponent()); |
| resolveInfo.defaultService = service; |
| break; |
| } |
| } |
| if (resolveInfo.defaultService == null) { |
| // If we didn't find the default, mark the first as default |
| // if there is only one. |
| if (resolveInfo.services.size() == 1) { |
| resolveInfo.defaultService = resolveInfo.services.get(0); |
| if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, " + |
| "routing to (default) " + resolveInfo.defaultService.getComponent()); |
| } else { |
| if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, routing all"); |
| } |
| } |
| } |
| return resolveInfo; |
| } |
| |
| void generateAidTreeLocked(List<ApduServiceInfo> services) { |
| // Easiest is to just build the entire tree again |
| mAidToServices.clear(); |
| for (ApduServiceInfo service : services) { |
| if (DBG) Log.d(TAG, "generateAidTree component: " + service.getComponent()); |
| for (String aid : service.getAids()) { |
| if (DBG) Log.d(TAG, "generateAidTree AID: " + aid); |
| // Check if a mapping exists for this AID |
| if (mAidToServices.containsKey(aid)) { |
| final ArrayList<ApduServiceInfo> aidServices = mAidToServices.get(aid); |
| aidServices.add(service); |
| } else { |
| final ArrayList<ApduServiceInfo> aidServices = |
| new ArrayList<ApduServiceInfo>(); |
| aidServices.add(service); |
| mAidToServices.put(aid, aidServices); |
| } |
| } |
| } |
| } |
| |
| void generateAidCategoriesLocked(List<ApduServiceInfo> services) { |
| // Trash existing mapping |
| mCategoryAids.clear(); |
| |
| for (ApduServiceInfo service : services) { |
| ArrayList<AidGroup> aidGroups = service.getAidGroups(); |
| if (aidGroups == null) continue; |
| for (AidGroup aidGroup : aidGroups) { |
| String groupCategory = aidGroup.getCategory(); |
| Set<String> categoryAids = mCategoryAids.get(groupCategory); |
| if (categoryAids == null) { |
| categoryAids = new HashSet<String>(); |
| } |
| categoryAids.addAll(aidGroup.getAids()); |
| mCategoryAids.put(groupCategory, categoryAids); |
| } |
| } |
| } |
| |
| boolean updateFromSettingsLocked(int userId) { |
| boolean changed = false; |
| |
| // Load current payment default from settings |
| String name = Settings.Secure.getStringForUser( |
| mContext.getContentResolver(), Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, |
| userId); |
| ComponentName newDefault = name != null ? ComponentName.unflattenFromString(name) : null; |
| ComponentName oldDefault = mCategoryDefaults.put(CardEmulation.CATEGORY_PAYMENT, |
| newDefault); |
| changed |= newDefault != oldDefault; |
| if (DBG) Log.d(TAG, "Updating default component to: " + (name != null ? |
| ComponentName.unflattenFromString(name) : "null")); |
| return changed; |
| } |
| |
| void generateAidCacheLocked() { |
| mAidCache.clear(); |
| for (Map.Entry<String, ArrayList<ApduServiceInfo>> aidEntry: |
| mAidToServices.entrySet()) { |
| String aid = aidEntry.getKey(); |
| if (!mAidCache.containsKey(aid)) { |
| mAidCache.put(aid, resolveAidLocked(aidEntry.getValue(), aid)); |
| } |
| } |
| } |
| |
| void updateRoutingLocked() { |
| final Set<String> handledAids = new HashSet<String>(); |
| // For each AID, find interested services |
| for (Map.Entry<String, AidResolveInfo> aidEntry: |
| mAidCache.entrySet()) { |
| String aid = aidEntry.getKey(); |
| AidResolveInfo resolveInfo = aidEntry.getValue(); |
| if (resolveInfo.services.size() == 0) { |
| // No interested services, if there is a current routing remove it |
| mRoutingManager.removeAid(aid); |
| } else if (resolveInfo.defaultService != null) { |
| // There is a default service set, route to that service |
| mRoutingManager.setRouteForAid(aid, resolveInfo.defaultService.isOnHost()); |
| } else if (resolveInfo.services.size() == 1) { |
| // Only one service, but not the default, must route to host |
| // to ask the user to confirm. |
| mRoutingManager.setRouteForAid(aid, true); |
| } else if (resolveInfo.services.size() > 1) { |
| // Multiple services, need to route to host to ask |
| mRoutingManager.setRouteForAid(aid, true); |
| } |
| handledAids.add(aid); |
| } |
| // Now, find AIDs in the routing table that are no longer routed to |
| // and remove them. |
| Set<String> routedAids = mRoutingManager.getRoutedAids(); |
| for (String aid : routedAids) { |
| if (!handledAids.contains(aid)) { |
| if (DBG) Log.d(TAG, "Removing routing for AID " + aid + ", because " + |
| "there are no no interested services."); |
| mRoutingManager.removeAid(aid); |
| } |
| } |
| // And commit the routing |
| mRoutingManager.commitRouting(); |
| } |
| |
| void showDefaultRemovedDialog() { |
| Intent intent = new Intent(mContext, DefaultRemovedActivity.class); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| mContext.startActivityAsUser(intent, UserHandle.CURRENT); |
| } |
| |
| void onPaymentDefaultRemoved(int userId, List<ApduServiceInfo> services) { |
| int numPaymentServices = 0; |
| ComponentName lastFoundPaymentService = null; |
| for (ApduServiceInfo service : services) { |
| if (service.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { |
| numPaymentServices++; |
| lastFoundPaymentService = service.getComponent(); |
| } |
| } |
| if (DBG) Log.d(TAG, "Number of payment services is " + |
| Integer.toString(numPaymentServices)); |
| if (numPaymentServices == 0) { |
| if (DBG) Log.d(TAG, "Default removed, no services left."); |
| // No payment services left, unset default and don't ask the user |
| setDefaultServiceForCategory(userId, null, |
| CardEmulation.CATEGORY_PAYMENT); |
| } else if (numPaymentServices == 1) { |
| // Only one left, automatically make it the default |
| if (DBG) Log.d(TAG, "Default removed, making remaining service default."); |
| setDefaultServiceForCategory(userId, lastFoundPaymentService, |
| CardEmulation.CATEGORY_PAYMENT); |
| } else if (numPaymentServices > 1) { |
| // More than one left, unset default and ask the user if he wants |
| // to set a new one |
| if (DBG) Log.d(TAG, "Default removed, asking user to pick."); |
| setDefaultServiceForCategory(userId, null, |
| CardEmulation.CATEGORY_PAYMENT); |
| showDefaultRemovedDialog(); |
| } |
| } |
| |
| void setDefaultIfNeededLocked(int userId, List<ApduServiceInfo> services) { |
| int numPaymentServices = 0; |
| ComponentName lastFoundPaymentService = null; |
| for (ApduServiceInfo service : services) { |
| if (service.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { |
| numPaymentServices++; |
| lastFoundPaymentService = service.getComponent(); |
| } |
| } |
| if (numPaymentServices > 1) { |
| // More than one service left, leave default unset |
| if (DBG) Log.d(TAG, "No default set, more than one service left."); |
| } else if (numPaymentServices == 1) { |
| // Make single found payment service the default |
| if (DBG) Log.d(TAG, "No default set, making single service default."); |
| setDefaultServiceForCategory(userId, lastFoundPaymentService, |
| CardEmulation.CATEGORY_PAYMENT); |
| } else { |
| // No payment services left, leave default at null |
| if (DBG) Log.d(TAG, "No default set, last payment service removed."); |
| } |
| } |
| |
| void checkDefaultsLocked(int userId, List<ApduServiceInfo> services) { |
| ComponentName defaultPaymentService = |
| getDefaultServiceForCategory(userId, CardEmulation.CATEGORY_PAYMENT, false); |
| Log.d(TAG, "Current default: " + defaultPaymentService); |
| if (defaultPaymentService != null) { |
| // Validate the default is still installed and handling payment |
| ApduServiceInfo serviceInfo = mServiceCache.getService(userId, defaultPaymentService); |
| if (serviceInfo == null) { |
| Log.e(TAG, "Default payment service unexpectedly removed."); |
| onPaymentDefaultRemoved(userId, services); |
| } else if (!serviceInfo.hasCategory(CardEmulation.CATEGORY_PAYMENT)) { |
| if (DBG) Log.d(TAG, "Default payment service had payment category removed"); |
| onPaymentDefaultRemoved(userId, services); |
| } else { |
| // Default still exists and handles the category, nothing do |
| if (DBG) Log.d(TAG, "Default payment service still ok."); |
| } |
| } else { |
| // A payment service may have been removed, leaving only one; |
| // in that case, automatically set that app as default. |
| setDefaultIfNeededLocked(userId, services); |
| } |
| } |
| |
| @Override |
| public void onServicesUpdated(int userId, List<ApduServiceInfo> services) { |
| synchronized (mLock) { |
| if (ActivityManager.getCurrentUser() == userId) { |
| // Rebuild our internal data-structures |
| checkDefaultsLocked(userId, services); |
| generateAidTreeLocked(services); |
| generateAidCategoriesLocked(services); |
| generateAidCacheLocked(); |
| updateRoutingLocked(); |
| } else { |
| if (DBG) Log.d(TAG, "Ignoring update because it's not for the current user."); |
| } |
| } |
| } |
| |
| public void invalidateCache(int currentUser) { |
| mServiceCache.invalidateCache(currentUser); |
| } |
| |
| public void onNfcDisabled() { |
| mContext.getContentResolver().unregisterContentObserver(mSettingsObserver); |
| mServiceCache.onNfcDisabled(); |
| mRoutingManager.onNfccRoutingTableCleared(); |
| } |
| |
| public void onNfcEnabled() { |
| mContext.getContentResolver().registerContentObserver( |
| Settings.Secure.getUriFor(Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT), |
| true, mSettingsObserver, UserHandle.USER_ALL); |
| synchronized (mLock) { |
| updateFromSettingsLocked(ActivityManager.getCurrentUser()); |
| } |
| mServiceCache.onNfcEnabled(); |
| } |
| } |