blob: 7618d38c3345393ed834eae206dd62f462d53c50 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.nfc.cardemulation;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.nfc.cardemulation.ApduServiceInfo;
import android.nfc.cardemulation.CardEmulation;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
import com.google.android.collect.Maps;
import java.util.Collections;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.PriorityQueue;
import java.util.TreeMap;
public class RegisteredAidCache {
static final String TAG = "RegisteredAidCache";
static final boolean DBG = false;
static final int AID_ROUTE_QUAL_SUBSET = 0x20;
static final int AID_ROUTE_QUAL_PREFIX = 0x10;
// mAidServices maps AIDs to services that have registered them.
// It's a TreeMap in order to be able to quickly select subsets
// of AIDs that conflict with each other.
final TreeMap<String, ArrayList<ServiceAidInfo>> mAidServices =
new TreeMap<String, ArrayList<ServiceAidInfo>>();
// mAidCache is a lookup table for quickly mapping an exact or prefix or subset AID
// to one or more handling 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 TreeMap<String, AidResolveInfo> mAidCache = new TreeMap<String, AidResolveInfo>();
// Represents a single AID registration of a service
final class ServiceAidInfo {
ApduServiceInfo service;
String aid;
String category;
@Override
public String toString() {
return "ServiceAidInfo{" +
"service=" + service.getComponent() +
", aid='" + aid + '\'' +
", category='" + category + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ServiceAidInfo that = (ServiceAidInfo) o;
if (!aid.equals(that.aid)) return false;
if (!category.equals(that.category)) return false;
if (!service.equals(that.service)) return false;
return true;
}
@Override
public int hashCode() {
int result = service.hashCode();
result = 31 * result + aid.hashCode();
result = 31 * result + category.hashCode();
return result;
}
}
// Represents a list of services, an optional default and a category that
// an AID was resolved to.
final class AidResolveInfo {
List<ApduServiceInfo> services = new ArrayList<ApduServiceInfo>();
ApduServiceInfo defaultService = null;
String category = null;
boolean mustRoute = true; // Whether this AID should be routed at all
ReslovedPrefixConflictAid prefixInfo = null;
@Override
public String toString() {
return "AidResolveInfo{" +
"services=" + services +
", defaultService=" + defaultService +
", category='" + category + '\'' +
", mustRoute=" + mustRoute +
'}';
}
}
final AidResolveInfo EMPTY_RESOLVE_INFO = new AidResolveInfo();
final Context mContext;
final AidRoutingManager mRoutingManager;
final Object mLock = new Object();
ComponentName mPreferredPaymentService;
ComponentName mPreferredForegroundService;
boolean mNfcEnabled = false;
boolean mSupportsPrefixes = false;
boolean mSupportsSubset = false;
public RegisteredAidCache(Context context) {
mContext = context;
mRoutingManager = new AidRoutingManager();
mPreferredPaymentService = null;
mPreferredForegroundService = null;
mSupportsPrefixes = mRoutingManager.supportsAidPrefixRouting();
mSupportsSubset = mRoutingManager.supportsAidSubsetRouting();
if (mSupportsPrefixes) {
if (DBG) Log.d(TAG, "Controller supports AID prefix routing");
}
if (mSupportsSubset) {
if (DBG) Log.d(TAG, "Controller supports AID subset routing");
}
}
public AidResolveInfo resolveAid(String aid) {
synchronized (mLock) {
if (DBG) Log.d(TAG, "resolveAid: resolving AID " + aid);
if (aid.length() < 10) {
Log.e(TAG, "AID selected with fewer than 5 bytes.");
return EMPTY_RESOLVE_INFO;
}
AidResolveInfo resolveInfo = new AidResolveInfo();
if (mSupportsPrefixes || mSupportsSubset) {
// Our AID cache may contain prefixes/subset which also match this AID,
// so we must find all potential prefixes or suffixes and merge the ResolveInfo
// of those prefixes plus any exact match in a single result.
String shortestAidMatch = aid.substring(0, 10); // Minimum AID length is 5 bytes
String longestAidMatch = String.format("%-32s", aid).replace(' ', 'F');
if (DBG) Log.d(TAG, "Finding AID registrations in range [" + shortestAidMatch +
" - " + longestAidMatch + "]");
NavigableMap<String, AidResolveInfo> matchingAids =
mAidCache.subMap(shortestAidMatch, true, longestAidMatch, true);
resolveInfo.category = CardEmulation.CATEGORY_OTHER;
for (Map.Entry<String, AidResolveInfo> entry : matchingAids.entrySet()) {
boolean isPrefix = isPrefix(entry.getKey());
boolean isSubset = isSubset(entry.getKey());
String entryAid = (isPrefix || isSubset) ? entry.getKey().substring(0,
entry.getKey().length() - 1):entry.getKey(); // Cut off '*' if prefix
if (entryAid.equalsIgnoreCase(aid) || (isPrefix && aid.startsWith(entryAid))
|| (isSubset && entryAid.startsWith(aid))) {
if (DBG) Log.d(TAG, "resolveAid: AID " + entry.getKey() + " matches.");
AidResolveInfo entryResolveInfo = entry.getValue();
if (entryResolveInfo.defaultService != null) {
if (resolveInfo.defaultService != null) {
// This shouldn't happen; for every prefix we have only one
// default service.
Log.e(TAG, "Different defaults for conflicting AIDs!");
}
resolveInfo.defaultService = entryResolveInfo.defaultService;
resolveInfo.category = entryResolveInfo.category;
}
for (ApduServiceInfo serviceInfo : entryResolveInfo.services) {
if (!resolveInfo.services.contains(serviceInfo)) {
resolveInfo.services.add(serviceInfo);
}
}
}
}
} else {
resolveInfo = mAidCache.get(aid);
}
if (DBG) Log.d(TAG, "Resolved to: " + resolveInfo);
return resolveInfo;
}
}
public boolean supportsAidPrefixRegistration() {
return mSupportsPrefixes;
}
public boolean supportsAidSubsetRegistration() {
return mSupportsSubset;
}
public boolean isDefaultServiceForAid(int userId, ComponentName service, String aid) {
AidResolveInfo resolveInfo = resolveAid(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;
}
}
/**
* Resolves a conflict between multiple services handling the same
* AIDs. Note that the AID itself is not an input to the decision
* process - the algorithm just looks at the competing services
* and what preferences the user has indicated. In short, it works like
* this:
*
* 1) If there is a preferred foreground service, that service wins
* 2) Else, if there is a preferred payment service, that service wins
* 3) Else, if there is no winner, and all conflicting services will be
* in the list of resolved services.
*/
AidResolveInfo resolveAidConflictLocked(Collection<ServiceAidInfo> conflictingServices,
boolean makeSingleServiceDefault) {
if (conflictingServices == null || conflictingServices.size() == 0) {
Log.e(TAG, "resolveAidConflict: No services passed in.");
return null;
}
AidResolveInfo resolveInfo = new AidResolveInfo();
resolveInfo.category = CardEmulation.CATEGORY_OTHER;
ApduServiceInfo matchedForeground = null;
ApduServiceInfo matchedPayment = null;
for (ServiceAidInfo serviceAidInfo : conflictingServices) {
boolean serviceClaimsPaymentAid =
CardEmulation.CATEGORY_PAYMENT.equals(serviceAidInfo.category);
if (serviceAidInfo.service.getComponent().equals(mPreferredForegroundService)) {
resolveInfo.services.add(serviceAidInfo.service);
if (serviceClaimsPaymentAid) {
resolveInfo.category = CardEmulation.CATEGORY_PAYMENT;
}
matchedForeground = serviceAidInfo.service;
} else if (serviceAidInfo.service.getComponent().equals(mPreferredPaymentService) &&
serviceClaimsPaymentAid) {
resolveInfo.services.add(serviceAidInfo.service);
resolveInfo.category = CardEmulation.CATEGORY_PAYMENT;
matchedPayment = serviceAidInfo.service;
} else {
if (serviceClaimsPaymentAid) {
// If this service claims it's a payment AID, don't route it,
// because it's not the default. Otherwise, add it to the list
// but not as default.
if (DBG) Log.d(TAG, "resolveAidLocked: (Ignoring handling service " +
serviceAidInfo.service.getComponent() +
" because it's not the payment default.)");
} else {
resolveInfo.services.add(serviceAidInfo.service);
}
}
}
if (matchedForeground != null) {
// 1st priority: if the foreground app prefers a service,
// and that service asks for the AID, that service gets it
if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to foreground preferred " +
matchedForeground);
resolveInfo.defaultService = matchedForeground;
} else if (matchedPayment != null) {
// 2nd priority: if there is a preferred payment service,
// and that service claims this as a payment AID, that service gets it
if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to payment default " +
"default " + matchedPayment);
resolveInfo.defaultService = matchedPayment;
} else {
if (resolveInfo.services.size() == 1 && makeSingleServiceDefault) {
if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: making single handling service " +
resolveInfo.services.get(0).getComponent() + " default.");
resolveInfo.defaultService = resolveInfo.services.get(0);
} else {
// Nothing to do, all services already in list
if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to all matching services");
}
}
return resolveInfo;
}
class DefaultServiceInfo {
ServiceAidInfo paymentDefault;
ServiceAidInfo foregroundDefault;
}
DefaultServiceInfo findDefaultServices(ArrayList<ServiceAidInfo> serviceAidInfos) {
DefaultServiceInfo defaultServiceInfo = new DefaultServiceInfo();
for (ServiceAidInfo serviceAidInfo : serviceAidInfos) {
boolean serviceClaimsPaymentAid =
CardEmulation.CATEGORY_PAYMENT.equals(serviceAidInfo.category);
if (serviceAidInfo.service.getComponent().equals(mPreferredForegroundService)) {
defaultServiceInfo.foregroundDefault = serviceAidInfo;
} else if (serviceAidInfo.service.getComponent().equals(mPreferredPaymentService) &&
serviceClaimsPaymentAid) {
defaultServiceInfo.paymentDefault = serviceAidInfo;
}
}
return defaultServiceInfo;
}
AidResolveInfo resolveAidConflictLocked(ArrayList<ServiceAidInfo> aidServices,
ArrayList<ServiceAidInfo> conflictingServices) {
// Find defaults among the root AID services themselves
DefaultServiceInfo aidDefaultInfo = findDefaultServices(aidServices);
// Find any defaults among the children
DefaultServiceInfo conflictingDefaultInfo = findDefaultServices(conflictingServices);
AidResolveInfo resolveinfo;
// Three conditions under which the root AID gets to be the default
// 1. A service registering the root AID is the current foreground preferred
// 2. A service registering the root AID is the current tap & pay default AND
// no child is the current foreground preferred
// 3. There is only one service for the root AID, and there are no children
if (aidDefaultInfo.foregroundDefault != null) {
if (DBG) Log.d(TAG, "Prefix AID service " +
aidDefaultInfo.foregroundDefault.service.getComponent() + " has foreground" +
" preference, ignoring conflicting AIDs.");
// Foreground default trumps any conflicting services, treat as normal AID conflict
// and ignore children
resolveinfo = resolveAidConflictLocked(aidServices, true);
//If the AID is subsetAID check for prefix in same service.
if (isSubset(aidServices.get(0).aid)) {
resolveinfo.prefixInfo = findPrefixConflictForSubsetAid(aidServices.get(0).aid ,
new ArrayList<ApduServiceInfo>(){{add(resolveinfo.defaultService);}},true);
}
return resolveinfo;
} else if (aidDefaultInfo.paymentDefault != null) {
// Check if any of the conflicting services is foreground default
if (conflictingDefaultInfo.foregroundDefault != null) {
// Conflicting AID registration is in foreground, trumps prefix tap&pay default
if (DBG) Log.d(TAG, "One of the conflicting AID registrations is foreground " +
"preferred, ignoring prefix.");
return EMPTY_RESOLVE_INFO;
} else {
// Prefix service is tap&pay default, treat as normal AID conflict for just prefix
if (DBG) Log.d(TAG, "Prefix AID service " +
aidDefaultInfo.paymentDefault.service.getComponent() + " is payment" +
" default, ignoring conflicting AIDs.");
resolveinfo = resolveAidConflictLocked(aidServices, true);
//If the AID is subsetAID check for prefix in same service.
if (isSubset(aidServices.get(0).aid)) {
resolveinfo.prefixInfo = findPrefixConflictForSubsetAid(aidServices.get(0).aid ,
new ArrayList<ApduServiceInfo>(){{add(resolveinfo.defaultService);}},true);
}
return resolveinfo;
}
} else {
if (conflictingDefaultInfo.foregroundDefault != null ||
conflictingDefaultInfo.paymentDefault != null) {
if (DBG) Log.d(TAG, "One of the conflicting AID registrations is either payment " +
"default or foreground preferred, ignoring prefix.");
return EMPTY_RESOLVE_INFO;
} else {
// No children that are preferred; add all services of the root
// make single service default if no children are present
if (DBG) Log.d(TAG, "No service has preference, adding all.");
resolveinfo = resolveAidConflictLocked(aidServices, conflictingServices.isEmpty());
//If the AID is subsetAID check for conflicting prefix in all
//conflciting services and root services.
if (isSubset(aidServices.get(0).aid)) {
ArrayList <ApduServiceInfo> apduServiceList = new ArrayList <ApduServiceInfo>();
for (ServiceAidInfo serviceInfo : conflictingServices)
apduServiceList.add(serviceInfo.service);
for (ServiceAidInfo serviceInfo : aidServices)
apduServiceList.add(serviceInfo.service);
resolveinfo.prefixInfo =
findPrefixConflictForSubsetAid(aidServices.get(0).aid ,apduServiceList,false);
}
return resolveinfo;
}
}
}
void generateServiceMapLocked(List<ApduServiceInfo> services) {
// Easiest is to just build the entire tree again
mAidServices.clear();
for (ApduServiceInfo service : services) {
if (DBG) Log.d(TAG, "generateServiceMap component: " + service.getComponent());
List<String> prefixAids = service.getPrefixAids();
List<String> subSetAids = service.getSubsetAids();
for (String aid : service.getAids()) {
if (!CardEmulation.isValidAid(aid)) {
Log.e(TAG, "Aid " + aid + " is not valid.");
continue;
}
if (aid.endsWith("*") && !supportsAidPrefixRegistration()) {
Log.e(TAG, "Prefix AID " + aid + " ignored on device that doesn't support it.");
continue;
} else if (supportsAidPrefixRegistration() && prefixAids.size() > 0 && isExact(aid)) {
// Check if we already have an overlapping prefix registered for this AID
boolean foundPrefix = false;
for (String prefixAid : prefixAids) {
String prefix = prefixAid.substring(0, prefixAid.length() - 1);
if (aid.startsWith(prefix)) {
Log.e(TAG, "Ignoring exact AID " + aid + " because prefix AID " + prefixAid +
" is already registered");
foundPrefix = true;
break;
}
}
if (foundPrefix) {
continue;
}
} else if (aid.endsWith("#") && !supportsAidSubsetRegistration()) {
Log.e(TAG, "Subset AID " + aid + " ignored on device that doesn't support it.");
continue;
} else if (supportsAidSubsetRegistration() && subSetAids.size() > 0 && isExact(aid)) {
// Check if we already have an overlapping subset registered for this AID
boolean foundSubset = false;
for (String subsetAid : subSetAids) {
String plainSubset = subsetAid.substring(0, subsetAid.length() - 1);
if (plainSubset.startsWith(aid)) {
Log.e(TAG, "Ignoring exact AID " + aid + " because subset AID " + plainSubset +
" is already registered");
foundSubset = true;
break;
}
}
if (foundSubset) {
continue;
}
}
ServiceAidInfo serviceAidInfo = new ServiceAidInfo();
serviceAidInfo.aid = aid.toUpperCase();
serviceAidInfo.service = service;
serviceAidInfo.category = service.getCategoryForAid(aid);
if (mAidServices.containsKey(serviceAidInfo.aid)) {
final ArrayList<ServiceAidInfo> serviceAidInfos =
mAidServices.get(serviceAidInfo.aid);
serviceAidInfos.add(serviceAidInfo);
} else {
final ArrayList<ServiceAidInfo> serviceAidInfos =
new ArrayList<ServiceAidInfo>();
serviceAidInfos.add(serviceAidInfo);
mAidServices.put(serviceAidInfo.aid, serviceAidInfos);
}
}
}
}
static boolean isExact(String aid) {
return (!((aid.endsWith("*") || (aid.endsWith("#")))));
}
static boolean isPrefix(String aid) {
return aid.endsWith("*");
}
static boolean isSubset(String aid) {
return aid.endsWith("#");
}
final class ReslovedPrefixConflictAid {
String prefixAid = null;
boolean matchingSubset = false;
}
final class AidConflicts {
NavigableMap<String, ArrayList<ServiceAidInfo>> conflictMap;
final ArrayList<ServiceAidInfo> services = new ArrayList<ServiceAidInfo>();
final HashSet<String> aids = new HashSet<String>();
}
ReslovedPrefixConflictAid findPrefixConflictForSubsetAid(String subsetAid ,
ArrayList<ApduServiceInfo> prefixServices, boolean priorityRootAid){
ArrayList<String> prefixAids = new ArrayList<String>();
String minPrefix = null;
//This functions checks whether there is a prefix AID matching to subset AID
//Because both the subset AID and matching smaller perfix are to be added to routing table.
//1.Finds the prefix matching AID in the services sent.
//2.Find the smallest prefix among matching prefix and add it only if it is not same as susbet AID.
//3..If the subset AID and prefix AID are same add only one AID with both prefix , subset bits set.
// Cut off "#"
String plainSubsetAid = subsetAid.substring(0, subsetAid.length() - 1);
for (ApduServiceInfo service : prefixServices) {
for (String prefixAid : service.getPrefixAids()) {
// Cut off "#"
String plainPrefix= prefixAid.substring(0, prefixAid.length() - 1);
if( plainSubsetAid.startsWith(plainPrefix)) {
if (priorityRootAid) {
if (CardEmulation.CATEGORY_PAYMENT.equals(service.getCategoryForAid(prefixAid)) ||
(service.getComponent().equals(mPreferredForegroundService)))
prefixAids.add(prefixAid);
} else {
prefixAids.add(prefixAid);
}
}
}
}
if (prefixAids.size() > 0)
minPrefix = Collections.min(prefixAids);
ReslovedPrefixConflictAid resolvedPrefix = new ReslovedPrefixConflictAid();
resolvedPrefix.prefixAid = minPrefix;
if ((minPrefix != null ) &&
plainSubsetAid.equalsIgnoreCase(minPrefix.substring(0, minPrefix.length() - 1)))
resolvedPrefix.matchingSubset = true;
return resolvedPrefix;
}
AidConflicts findConflictsForPrefixLocked(String prefixAid) {
AidConflicts prefixConflicts = new AidConflicts();
String plainAid = prefixAid.substring(0, prefixAid.length() - 1); // Cut off "*"
String lastAidWithPrefix = String.format("%-32s", plainAid).replace(' ', 'F');
if (DBG) Log.d(TAG, "Finding AIDs in range [" + plainAid + " - " +
lastAidWithPrefix + "]");
prefixConflicts.conflictMap =
mAidServices.subMap(plainAid, true, lastAidWithPrefix, true);
for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
prefixConflicts.conflictMap.entrySet()) {
if (!entry.getKey().equalsIgnoreCase(prefixAid)) {
if (DBG)
Log.d(TAG, "AID " + entry.getKey() + " conflicts with prefix; " +
" adding handling services for conflict resolution.");
prefixConflicts.services.addAll(entry.getValue());
prefixConflicts.aids.add(entry.getKey());
}
}
return prefixConflicts;
}
AidConflicts findConflictsForSubsetAidLocked(String subsetAid) {
AidConflicts subsetConflicts = new AidConflicts();
// Cut off "@"
String lastPlainAid = subsetAid.substring(0, subsetAid.length() - 1);
// Cut off "@"
String plainSubsetAid = subsetAid.substring(0, subsetAid.length() - 1);
String firstAid = subsetAid.substring(0, 10);
if (DBG) Log.d(TAG, "Finding AIDs in range [" + firstAid + " - " +
lastPlainAid + "]");
subsetConflicts.conflictMap = new TreeMap();
for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
mAidServices.entrySet()) {
String aid = entry.getKey();
String plainAid = aid;
if (isSubset(aid) || isPrefix(aid))
plainAid = aid.substring(0, aid.length() - 1);
if (plainSubsetAid.startsWith(plainAid))
subsetConflicts.conflictMap.put(entry.getKey(),entry.getValue());
}
for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
subsetConflicts.conflictMap.entrySet()) {
if (!entry.getKey().equalsIgnoreCase(subsetAid)) {
if (DBG)
Log.d(TAG, "AID " + entry.getKey() + " conflicts with subset AID; " +
" adding handling services for conflict resolution.");
subsetConflicts.services.addAll(entry.getValue());
subsetConflicts.aids.add(entry.getKey());
}
}
return subsetConflicts;
}
void generateAidCacheLocked() {
mAidCache.clear();
// Get all exact and prefix AIDs in an ordered list
final TreeMap<String, AidResolveInfo> aidCache = new TreeMap<String, AidResolveInfo>();
//aidCache is temproary cache for geenrating the first prefix based lookup table.
PriorityQueue<String> aidsToResolve = new PriorityQueue<String>(mAidServices.keySet());
aidCache.clear();
while (!aidsToResolve.isEmpty()) {
final ArrayList<String> resolvedAids = new ArrayList<String>();
String aidToResolve = aidsToResolve.peek();
// Because of the lexicographical ordering, all following AIDs either start with the
// same bytes and are longer, or start with different bytes.
// A special case is if another service registered the same AID as a prefix, in
// which case we want to start with that AID, since it conflicts with this one
// All exact and suffix and prefix AID must be checked for conflicting cases
if (aidsToResolve.contains(aidToResolve + "*")) {
aidToResolve = aidToResolve + "*";
}
if (DBG) Log.d(TAG, "generateAidCacheLocked: starting with aid " + aidToResolve);
if (isPrefix(aidToResolve)) {
// This AID itself is a prefix; let's consider this prefix as the "root",
// and all conflicting AIDs as its children.
// For example, if "A000000003*" is the prefix root,
// "A000000003", "A00000000301*", "A0000000030102" are all conflicting children AIDs
final ArrayList<ServiceAidInfo> prefixServices = new ArrayList<ServiceAidInfo>(
mAidServices.get(aidToResolve));
// Find all conflicting children services
AidConflicts prefixConflicts = findConflictsForPrefixLocked(aidToResolve);
// Resolve conflicts
AidResolveInfo resolveInfo = resolveAidConflictLocked(prefixServices,
prefixConflicts.services);
aidCache.put(aidToResolve, resolveInfo);
resolvedAids.add(aidToResolve);
if (resolveInfo.defaultService != null) {
// This prefix is the default; therefore, AIDs of all conflicting children
// will no longer be evaluated.
resolvedAids.addAll(prefixConflicts.aids);
for (String aid : resolveInfo.defaultService.getSubsetAids()) {
if (prefixConflicts.aids.contains(aid)) {
if ((CardEmulation.CATEGORY_PAYMENT.equals(resolveInfo.defaultService.getCategoryForAid(aid))) ||
(resolveInfo.defaultService.getComponent().equals(mPreferredForegroundService))) {
AidResolveInfo childResolveInfo = resolveAidConflictLocked(mAidServices.get(aid), false);
aidCache.put(aid,childResolveInfo);
Log.d(TAG, "AID " + aid+ " shared with prefix; " +
"adding subset .");
}
}
}
} else if (resolveInfo.services.size() > 0) {
// This means we don't have a default for this prefix and all its
// conflicting children. So, for all conflicting AIDs, just add
// all handling services without setting a default
boolean foundChildService = false;
for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
prefixConflicts.conflictMap.entrySet()) {
if (!entry.getKey().equalsIgnoreCase(aidToResolve)) {
if (DBG)
Log.d(TAG, "AID " + entry.getKey() + " shared with prefix; " +
" adding all handling services.");
AidResolveInfo childResolveInfo = resolveAidConflictLocked(
entry.getValue(), false);
// Special case: in this case all children AIDs must be routed to the
// host, so we can ask the user which service is preferred.
// Since these are all "children" of the prefix, they don't need
// to be routed, since the prefix will already get routed to the host
childResolveInfo.mustRoute = false;
aidCache.put(entry.getKey(),childResolveInfo);
resolvedAids.add(entry.getKey());
foundChildService |= !childResolveInfo.services.isEmpty();
}
}
// Special case: if in the end we didn't add any children services,
// and the prefix has only one service, make that default
if (!foundChildService && resolveInfo.services.size() == 1) {
resolveInfo.defaultService = resolveInfo.services.get(0);
}
} else {
// This prefix is not handled at all; we will evaluate
// the children separately in next passes.
}
} else {
// Exact AID and no other conflicting AID registrations present
// This is true because aidsToResolve is lexicographically ordered, and
// so by necessity all other AIDs are different than this AID or longer.
if (DBG) Log.d(TAG, "Exact AID, resolving.");
final ArrayList<ServiceAidInfo> conflictingServiceInfos =
new ArrayList<ServiceAidInfo>(mAidServices.get(aidToResolve));
aidCache.put(aidToResolve, resolveAidConflictLocked(conflictingServiceInfos, true));
resolvedAids.add(aidToResolve);
}
// Remove the AIDs we resolved from the list of AIDs to resolve
if (DBG) Log.d(TAG, "AIDs: " + resolvedAids + " were resolved.");
aidsToResolve.removeAll(resolvedAids);
resolvedAids.clear();
}
PriorityQueue<String> reversedQueue = new PriorityQueue<String>(1, Collections.reverseOrder());
reversedQueue.addAll(aidCache.keySet());
while (!reversedQueue.isEmpty()) {
final ArrayList<String> resolvedAids = new ArrayList<String>();
String aidToResolve = reversedQueue.peek();
if (isPrefix(aidToResolve)) {
String matchingSubset = aidToResolve.substring(0,aidToResolve.length()-1 ) + "#";
if (DBG) Log.d(TAG, "matching subset"+matchingSubset);
if (reversedQueue.contains(matchingSubset))
aidToResolve = aidToResolve.substring(0,aidToResolve.length()-1) + "#";
}
if (isSubset(aidToResolve)) {
if (DBG) Log.d(TAG, "subset resolving aidToResolve "+aidToResolve);
final ArrayList<ServiceAidInfo> subsetServices = new ArrayList<ServiceAidInfo>(
mAidServices.get(aidToResolve));
// Find all conflicting children services
AidConflicts aidConflicts = findConflictsForSubsetAidLocked(aidToResolve);
// Resolve conflicts
AidResolveInfo resolveInfo = resolveAidConflictLocked(subsetServices,
aidConflicts.services);
mAidCache.put(aidToResolve, resolveInfo);
resolvedAids.add(aidToResolve);
if (resolveInfo.defaultService != null) {
// This subset is the default; therefore, AIDs of all conflicting children
// will no longer be evaluated.Check for any prefix matching in the same service
if (resolveInfo.prefixInfo != null && resolveInfo.prefixInfo.prefixAid != null &&
!resolveInfo.prefixInfo.matchingSubset) {
if (DBG)
Log.d(TAG, "AID default " + resolveInfo.prefixInfo.prefixAid +
" prefix AID shared with dsubset root; " +
" adding prefix aid");
AidResolveInfo childResolveInfo = resolveAidConflictLocked(
mAidServices.get(resolveInfo.prefixInfo.prefixAid), false);
mAidCache.put(resolveInfo.prefixInfo.prefixAid, childResolveInfo);
}
resolvedAids.addAll(aidConflicts.aids);
} else if (resolveInfo.services.size() > 0) {
// This means we don't have a default for this subset and all its
// conflicting children. So, for all conflicting AIDs, just add
// all handling services without setting a default
boolean foundChildService = false;
for (Map.Entry<String, ArrayList<ServiceAidInfo>> entry :
aidConflicts.conflictMap.entrySet()) {
// We need to add shortest prefix among them.
if (!entry.getKey().equalsIgnoreCase(aidToResolve)) {
if (DBG)
Log.d(TAG, "AID " + entry.getKey() + " shared with subset root; " +
" adding all handling services.");
AidResolveInfo childResolveInfo = resolveAidConflictLocked(
entry.getValue(), false);
// Special case: in this case all children AIDs must be routed to the
// host, so we can ask the user which service is preferred.
// Since these are all "children" of the subset, they don't need
// to be routed, since the subset will already get routed to the host
childResolveInfo.mustRoute = false;
mAidCache.put(entry.getKey(),childResolveInfo);
resolvedAids.add(entry.getKey());
foundChildService |= !childResolveInfo.services.isEmpty();
}
}
if(resolveInfo.prefixInfo != null &&
resolveInfo.prefixInfo.prefixAid != null &&
!resolveInfo.prefixInfo.matchingSubset) {
AidResolveInfo childResolveInfo = resolveAidConflictLocked(
mAidServices.get(resolveInfo.prefixInfo.prefixAid), false);
mAidCache.put(resolveInfo.prefixInfo.prefixAid, childResolveInfo);
if (DBG)
Log.d(TAG, "AID " + resolveInfo.prefixInfo.prefixAid +
" prefix AID shared with subset root; " +
" adding prefix aid");
}
// Special case: if in the end we didn't add any children services,
// and the subset has only one service, make that default
if (!foundChildService && resolveInfo.services.size() == 1) {
resolveInfo.defaultService = resolveInfo.services.get(0);
}
} else {
// This subset is not handled at all; we will evaluate
// the children separately in next passes.
}
} else {
// Exact AID and no other conflicting AID registrations present. This is
// true because reversedQueue is lexicographically ordered in revrese, and
// so by necessity all other AIDs are different than this AID or shorter.
if (DBG) Log.d(TAG, "Exact or Prefix AID."+aidToResolve);
mAidCache.put(aidToResolve, aidCache.get(aidToResolve));
resolvedAids.add(aidToResolve);
}
// Remove the AIDs we resolved from the list of AIDs to resolve
if (DBG) Log.d(TAG, "AIDs: " + resolvedAids + " were resolved.");
reversedQueue.removeAll(resolvedAids);
resolvedAids.clear();
}
updateRoutingLocked(false);
}
void updateRoutingLocked(boolean force) {
if (!mNfcEnabled) {
if (DBG) Log.d(TAG, "Not updating routing table because NFC is off.");
return;
}
final HashMap<String, AidRoutingManager.AidEntry> routingEntries = Maps.newHashMap();
// For each AID, find interested services
for (Map.Entry<String, AidResolveInfo> aidEntry:
mAidCache.entrySet()) {
String aid = aidEntry.getKey();
AidResolveInfo resolveInfo = aidEntry.getValue();
if (!resolveInfo.mustRoute) {
if (DBG) Log.d(TAG, "Not routing AID " + aid + " on request.");
continue;
}
AidRoutingManager.AidEntry aidType = mRoutingManager.new AidEntry();
if (aid.endsWith("#")) {
aidType.aidInfo |= AID_ROUTE_QUAL_SUBSET;
}
if(aid.endsWith("*") || (resolveInfo.prefixInfo != null &&
resolveInfo.prefixInfo.matchingSubset)) {
aidType.aidInfo |= AID_ROUTE_QUAL_PREFIX;
}
if (resolveInfo.services.size() == 0) {
// No interested services
} else if (resolveInfo.defaultService != null) {
// There is a default service set, route to where that service resides -
// either on the host (HCE) or on an SE.
aidType.isOnHost = resolveInfo.defaultService.isOnHost();
if (!aidType.isOnHost) {
aidType.offHostSE =
resolveInfo.defaultService.getOffHostSecureElement();
}
routingEntries.put(aid, aidType);
} else if (resolveInfo.services.size() == 1) {
// Only one service, but not the default, must route to host
// to ask the user to choose one.
if (resolveInfo.category.equals(
CardEmulation.CATEGORY_PAYMENT)) {
aidType.isOnHost = true;
} else {
aidType.isOnHost = resolveInfo.services.get(0).isOnHost();
if (!aidType.isOnHost) {
aidType.offHostSE =
resolveInfo.services.get(0).getOffHostSecureElement();
}
}
routingEntries.put(aid, aidType);
} else if (resolveInfo.services.size() > 1) {
// Multiple services if all the services are routing to same
// offhost then the service should be routed to off host.
boolean onHost = false;
String offHostSE = null;
for (ApduServiceInfo service : resolveInfo.services) {
// In case there is at least one service which routes to host
// Route it to host for user to select which service to use
onHost |= service.isOnHost();
if (!onHost) {
if (offHostSE == null) {
offHostSE = service.getOffHostSecureElement();
} else if (!offHostSE.equals(
service.getOffHostSecureElement())) {
// There are registerations to different SEs, route this
// to host and have user choose a service for this AID
offHostSE = null;
onHost = true;
break;
}
}
}
aidType.isOnHost = onHost;
aidType.offHostSE = onHost ? null : offHostSE;
routingEntries.put(aid, aidType);
}
}
mRoutingManager.configureRouting(routingEntries, force);
}
public void onServicesUpdated(int userId, List<ApduServiceInfo> services) {
if (DBG) Log.d(TAG, "onServicesUpdated");
synchronized (mLock) {
if (ActivityManager.getCurrentUser() == userId) {
// Rebuild our internal data-structures
generateServiceMapLocked(services);
generateAidCacheLocked();
} else {
if (DBG) Log.d(TAG, "Ignoring update because it's not for the current user.");
}
}
}
public void onPreferredPaymentServiceChanged(ComponentName service) {
if (DBG) Log.d(TAG, "Preferred payment service changed.");
synchronized (mLock) {
mPreferredPaymentService = service;
generateAidCacheLocked();
}
}
public void onPreferredForegroundServiceChanged(ComponentName service) {
if (DBG) Log.d(TAG, "Preferred foreground service changed.");
synchronized (mLock) {
mPreferredForegroundService = service;
generateAidCacheLocked();
}
}
public ComponentName getPreferredService() {
if (mPreferredForegroundService != null) {
// return current foreground service
return mPreferredForegroundService;
} else {
// return current preferred service
return mPreferredPaymentService;
}
}
public void onNfcDisabled() {
synchronized (mLock) {
mNfcEnabled = false;
}
mRoutingManager.onNfccRoutingTableCleared();
}
public void onNfcEnabled() {
synchronized (mLock) {
mNfcEnabled = true;
updateRoutingLocked(false);
}
}
public void onSecureNfcToggled() {
synchronized (mLock) {
updateRoutingLocked(true);
}
}
String dumpEntry(Map.Entry<String, AidResolveInfo> entry) {
StringBuilder sb = new StringBuilder();
String category = entry.getValue().category;
ApduServiceInfo defaultServiceInfo = entry.getValue().defaultService;
sb.append(" \"" + entry.getKey() + "\" (category: " + category + ")\n");
ComponentName defaultComponent = defaultServiceInfo != null ?
defaultServiceInfo.getComponent() : null;
for (ApduServiceInfo serviceInfo : entry.getValue().services) {
sb.append(" ");
if (serviceInfo.getComponent().equals(defaultComponent)) {
sb.append("*DEFAULT* ");
}
sb.append(serviceInfo.getComponent() +
" (Description: " + serviceInfo.getDescription() + ")\n");
}
return sb.toString();
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println(" AID cache entries: ");
for (Map.Entry<String, AidResolveInfo> entry : mAidCache.entrySet()) {
pw.println(dumpEntry(entry));
}
pw.println(" Service preferred by foreground app: " + mPreferredForegroundService);
pw.println(" Preferred payment service: " + mPreferredPaymentService);
pw.println("");
mRoutingManager.dump(fd, pw, args);
pw.println("");
}
/**
* Dump debugging information as a RegisteredAidCacheProto
*
* Note:
* See proto definition in frameworks/base/core/proto/android/nfc/card_emulation.proto
* When writing a nested message, must call {@link ProtoOutputStream#start(long)} before and
* {@link ProtoOutputStream#end(long)} after.
* Never reuse a proto field number. When removing a field, mark it as reserved.
*/
void dumpDebug(ProtoOutputStream proto) {
for (Map.Entry<String, AidResolveInfo> entry : mAidCache.entrySet()) {
long token = proto.start(RegisteredAidCacheProto.AID_CACHE_ENTRIES);
proto.write(RegisteredAidCacheProto.AidCacheEntry.KEY, entry.getKey());
proto.write(RegisteredAidCacheProto.AidCacheEntry.CATEGORY, entry.getValue().category);
ApduServiceInfo defaultServiceInfo = entry.getValue().defaultService;
ComponentName defaultComponent = defaultServiceInfo != null ?
defaultServiceInfo.getComponent() : null;
if (defaultComponent != null) {
defaultComponent.dumpDebug(proto,
RegisteredAidCacheProto.AidCacheEntry.DEFAULT_COMPONENT);
}
for (ApduServiceInfo serviceInfo : entry.getValue().services) {
long sToken = proto.start(RegisteredAidCacheProto.AidCacheEntry.SERVICES);
serviceInfo.dumpDebug(proto);
proto.end(sToken);
}
proto.end(token);
}
if (mPreferredForegroundService != null) {
mPreferredForegroundService.dumpDebug(proto,
RegisteredAidCacheProto.PREFERRED_FOREGROUND_SERVICE);
}
if (mPreferredPaymentService != null) {
mPreferredPaymentService.dumpDebug(proto,
RegisteredAidCacheProto.PREFERRED_PAYMENT_SERVICE);
}
long token = proto.start(RegisteredAidCacheProto.ROUTING_MANAGER);
mRoutingManager.dumpDebug(proto);
proto.end(token);
}
}