blob: e3607743080b13400b60001946788722b8f06c63 [file] [log] [blame]
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.io.FileDescriptor;
import java.io.PrintWriter;
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 = false;
// 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;
boolean mNfcEnabled = false;
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;
mContext.getContentResolver().registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT),
true, mSettingsObserver, UserHandle.USER_ALL);
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 = null;
boolean serviceFound = false;
synchronized (mLock) {
serviceFound = mServiceCache.hasService(userId, service);
}
if (!serviceFound) {
// If we don't know about this service yet, it may have just been enabled
// using PackageManager.setComponentEnabledSetting(). The PackageManager
// broadcasts are delayed by 10 seconds in that scenario, which causes
// calls to our APIs referencing that service to fail.
// Hence, update the cache in case we don't know about the service.
if (DBG) Log.d(TAG, "Didn't find passed in service, invalidating cache.");
mServiceCache.invalidateCache(userId);
}
synchronized (mLock) {
resolveInfo = mAidCache.get(aid);
}
if (resolveInfo == null || 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 boolean isDefaultServiceForCategory(int userId, String category,
ComponentName service) {
boolean serviceFound = false;
synchronized (mLock) {
// If we don't know about this service yet, it may have just been enabled
// using PackageManager.setComponentEnabledSetting(). The PackageManager
// broadcasts are delayed by 10 seconds in that scenario, which causes
// calls to our APIs referencing that service to fail.
// Hence, update the cache in case we don't know about the service.
serviceFound = mServiceCache.hasService(userId, service);
}
if (!serviceFound) {
if (DBG) Log.d(TAG, "Didn't find passed in service, invalidating cache.");
mServiceCache.invalidateCache(userId);
}
ComponentName defaultService =
getDefaultServiceForCategory(userId, category, true);
return (defaultService != null && defaultService.equals(service));
}
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();
if (DBG) Log.d(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);
if (DBG) 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) {
// 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);
if (DBG) Log.d(TAG, "Updating default component to: " + (name != null ?
ComponentName.unflattenFromString(name) : "null"));
return newDefault != oldDefault;
}
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() {
if (!mNfcEnabled) {
if (DBG) Log.d(TAG, "Not updating routing table because NFC is off.");
return;
}
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);
if (DBG) 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);
}
updateFromSettingsLocked(userId);
}
@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() {
synchronized (mLock) {
mNfcEnabled = false;
}
mServiceCache.onNfcDisabled();
mRoutingManager.onNfccRoutingTableCleared();
}
public void onNfcEnabled() {
synchronized (mLock) {
mNfcEnabled = true;
updateFromSettingsLocked(ActivityManager.getCurrentUser());
}
mServiceCache.onNfcEnabled();
}
String dumpEntry(Map.Entry<String, AidResolveInfo> entry) {
StringBuilder sb = new StringBuilder();
sb.append(" \"" + entry.getKey() + "\"\n");
ApduServiceInfo defaultService = entry.getValue().defaultService;
ComponentName defaultComponent = defaultService != null ?
defaultService.getComponent() : null;
for (ApduServiceInfo service : entry.getValue().services) {
sb.append(" ");
if (service.getComponent().equals(defaultComponent)) {
sb.append("*DEFAULT* ");
}
sb.append(service.getComponent() +
" (Description: " + service.getDescription() + ")\n");
}
return sb.toString();
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
mServiceCache.dump(fd, pw, args);
pw.println("AID cache entries: ");
for (Map.Entry<String, AidResolveInfo> entry : mAidCache.entrySet()) {
pw.println(dumpEntry(entry));
}
pw.println("Category defaults: ");
for (Map.Entry<String, ComponentName> entry : mCategoryDefaults.entrySet()) {
pw.println(" " + entry.getKey() + "->" + entry.getValue());
}
pw.println("");
}
}