RegisteredAidCache.java revision e6f2dc6731bccc8ea01c8d9d03dd7e90f2c00312
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.nfc.cardemulation; 18 19import android.app.ActivityManager; 20import android.content.ComponentName; 21import android.content.Context; 22import android.nfc.cardemulation.AidGroup; 23import android.nfc.cardemulation.ApduServiceInfo; 24import android.nfc.cardemulation.CardEmulation; 25import android.util.Log; 26 27import com.google.android.collect.Maps; 28 29import java.io.FileDescriptor; 30import java.io.PrintWriter; 31import java.util.ArrayList; 32import java.util.HashMap; 33import java.util.HashSet; 34import java.util.List; 35import java.util.Map; 36import java.util.Set; 37import java.util.SortedMap; 38import java.util.TreeMap; 39 40public class RegisteredAidCache { 41 static final String TAG = "RegisteredAidCache"; 42 43 static final boolean DBG = false; 44 45 // mAidServices is a tree that maps an AID to a list of handling services 46 // on Android. It is only valid for the current user. 47 final TreeMap<String, ArrayList<ApduServiceInfo>> mAidToServices = 48 new TreeMap<String, ArrayList<ApduServiceInfo>>(); 49 50 // mAidCache is a lookup table for quickly mapping an AID to one or 51 // more services. It differs from mAidServices in the sense that it 52 // has already accounted for defaults, and hence its return value 53 // is authoritative for the current set of services and defaults. 54 // It is only valid for the current user. 55 final HashMap<String, AidResolveInfo> mAidCache = 56 Maps.newHashMap(); 57 58 final class AidResolveInfo { 59 List<ApduServiceInfo> services; 60 ApduServiceInfo defaultService; 61 String aid; 62 } 63 64 final Context mContext; 65 final AidRoutingManager mRoutingManager; 66 67 final Object mLock = new Object(); 68 /** 69 * AIDs per category 70 */ 71 public final HashMap<String, Set<String>> mCategoryAids = 72 Maps.newHashMap(); 73 74 ComponentName mPreferredPaymentService; 75 ComponentName mPreferredForegroundService; 76 77 boolean mNfcEnabled = false; 78 79 public RegisteredAidCache(Context context) { 80 mContext = context; 81 mRoutingManager = new AidRoutingManager(); 82 mPreferredPaymentService = null; 83 mPreferredForegroundService = null; 84 } 85 86 public AidResolveInfo resolveAidPrefix(String aid) { 87 synchronized (mLock) { 88 char nextAidChar = (char) (aid.charAt(aid.length() - 1) + 1); 89 String nextAid = aid.substring(0, aid.length() - 1) + nextAidChar; 90 SortedMap<String, ArrayList<ApduServiceInfo>> matches = 91 mAidToServices.subMap(aid, nextAid); 92 // The first match is lexicographically closest to what the reader asked; 93 if (matches.isEmpty()) { 94 return null; 95 } else { 96 AidResolveInfo resolveInfo = mAidCache.get(matches.firstKey()); 97 // Let the caller know which AID got selected 98 resolveInfo.aid = matches.firstKey(); 99 return resolveInfo; 100 } 101 } 102 } 103 104 public String getCategoryForAid(String aid) { 105 synchronized (mLock) { 106 Set<String> paymentAids = mCategoryAids.get(CardEmulation.CATEGORY_PAYMENT); 107 if (paymentAids != null && paymentAids.contains(aid)) { 108 return CardEmulation.CATEGORY_PAYMENT; 109 } else { 110 return CardEmulation.CATEGORY_OTHER; 111 } 112 } 113 } 114 115 public boolean isDefaultServiceForAid(int userId, ComponentName service, String aid) { 116 AidResolveInfo resolveInfo = null; 117 synchronized (mLock) { 118 resolveInfo = mAidCache.get(aid); 119 } 120 if (resolveInfo == null || resolveInfo.services == null || 121 resolveInfo.services.size() == 0) { 122 return false; 123 } 124 125 if (resolveInfo.defaultService != null) { 126 return service.equals(resolveInfo.defaultService.getComponent()); 127 } else if (resolveInfo.services.size() == 1) { 128 return service.equals(resolveInfo.services.get(0).getComponent()); 129 } else { 130 // More than one service, not the default 131 return false; 132 } 133 } 134 135 /** 136 * Resolves an AID to a set of services that can handle it. 137 * Takes into account conflict resolution modes per category 138 * and defaults. 139 */ 140 AidResolveInfo resolveAidLocked(List<ApduServiceInfo> handlingServices, String aid) { 141 if (handlingServices == null || handlingServices.size() == 0) { 142 if (DBG) Log.d(TAG, "Could not resolve AID " + aid + " to any service."); 143 return null; 144 } 145 if (DBG) Log.d(TAG, "resolveAidLocked: resolving AID " + aid); 146 AidResolveInfo resolveInfo = new AidResolveInfo(); 147 resolveInfo.services = new ArrayList<ApduServiceInfo>(); 148 resolveInfo.defaultService = null; 149 150 ApduServiceInfo matchedForeground = null; 151 ApduServiceInfo matchedPayment = null; 152 for (ApduServiceInfo service : handlingServices) { 153 boolean serviceClaimsPaymentAid = 154 CardEmulation.CATEGORY_PAYMENT.equals(service.getCategoryForAid(aid)); 155 if (service.getComponent().equals(mPreferredForegroundService)) { 156 resolveInfo.services.add(service); 157 matchedForeground = service; 158 } else if (service.getComponent().equals(mPreferredPaymentService) && 159 serviceClaimsPaymentAid) { 160 resolveInfo.services.add(service); 161 matchedPayment = service; 162 } else { 163 if (serviceClaimsPaymentAid) { 164 // If this service claims it's a payment AID, don't route it, 165 // because it's not the default. Otherwise, add it to the list 166 // but not as default. 167 if (DBG) Log.d(TAG, "resolveAidLocked: (Ignoring handling service " + 168 service.getComponent() + " because it's not the payment default.)"); 169 } else { 170 resolveInfo.services.add(service); 171 } 172 } 173 } 174 if (matchedForeground != null) { 175 // 1st priority: if the foreground app prefers a service, 176 // and that service asks for the AID, that service gets it 177 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to foreground preferred " + 178 matchedForeground); 179 resolveInfo.defaultService = matchedForeground; 180 } else if (matchedPayment != null) { 181 // 2nd priority: if there is a preferred payment service, 182 // and that service claims this as a payment AID, that service gets it 183 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to payment default " + 184 "default " + matchedPayment); 185 resolveInfo.defaultService = matchedPayment; 186 } else { 187 // If there's only one service left handling the AID, that service gets it by default 188 if (resolveInfo.services.size() == 1) { 189 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to single matching " + 190 "service"); 191 resolveInfo.defaultService = handlingServices.get(0); 192 } else { 193 // Nothing to do, all services already in list 194 if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to all matching " + 195 "services"); 196 } 197 } 198 return resolveInfo; 199 } 200 201 void generateAidTreeLocked(List<ApduServiceInfo> services) { 202 // Easiest is to just build the entire tree again 203 mAidToServices.clear(); 204 for (ApduServiceInfo service : services) { 205 if (DBG) Log.d(TAG, "generateAidTree component: " + service.getComponent()); 206 for (String aid : service.getAids()) { 207 if (DBG) Log.d(TAG, "generateAidTree AID: " + aid); 208 // Check if a mapping exists for this AID 209 if (mAidToServices.containsKey(aid)) { 210 final ArrayList<ApduServiceInfo> aidServices = mAidToServices.get(aid); 211 aidServices.add(service); 212 } else { 213 final ArrayList<ApduServiceInfo> aidServices = 214 new ArrayList<ApduServiceInfo>(); 215 aidServices.add(service); 216 mAidToServices.put(aid, aidServices); 217 } 218 } 219 } 220 } 221 222 void generateAidCategoriesLocked(List<ApduServiceInfo> services) { 223 // Trash existing mapping 224 mCategoryAids.clear(); 225 226 for (ApduServiceInfo service : services) { 227 ArrayList<AidGroup> aidGroups = service.getAidGroups(); 228 if (aidGroups == null) continue; 229 for (AidGroup aidGroup : aidGroups) { 230 String groupCategory = aidGroup.getCategory(); 231 Set<String> categoryAids = mCategoryAids.get(groupCategory); 232 if (categoryAids == null) { 233 categoryAids = new HashSet<String>(); 234 } 235 categoryAids.addAll(aidGroup.getAids()); 236 mCategoryAids.put(groupCategory, categoryAids); 237 } 238 } 239 } 240 241 void generateAidCacheLocked() { 242 mAidCache.clear(); 243 for (Map.Entry<String, ArrayList<ApduServiceInfo>> aidEntry: 244 mAidToServices.entrySet()) { 245 String aid = aidEntry.getKey(); 246 if (!mAidCache.containsKey(aid)) { 247 mAidCache.put(aid, resolveAidLocked(aidEntry.getValue(), aid)); 248 } 249 } 250 updateRoutingLocked(); 251 } 252 253 void updateRoutingLocked() { 254 if (!mNfcEnabled) { 255 if (DBG) Log.d(TAG, "Not updating routing table because NFC is off."); 256 return; 257 } 258 final Set<String> handledAids = new HashSet<String>(); 259 // For each AID, find interested services 260 for (Map.Entry<String, AidResolveInfo> aidEntry: 261 mAidCache.entrySet()) { 262 String aid = aidEntry.getKey(); 263 AidResolveInfo resolveInfo = aidEntry.getValue(); 264 if (resolveInfo.services.size() == 0) { 265 // No interested services, if there is a current routing remove it 266 mRoutingManager.removeAid(aid); 267 } else if (resolveInfo.defaultService != null) { 268 // There is a default service set, route to that service 269 mRoutingManager.setRouteForAid(aid, resolveInfo.defaultService.isOnHost()); 270 } else if (resolveInfo.services.size() == 1) { 271 // Only one service, but not the default, must route to host 272 // to ask the user to confirm. 273 mRoutingManager.setRouteForAid(aid, true); 274 } else if (resolveInfo.services.size() > 1) { 275 // Multiple services, need to route to host to ask 276 mRoutingManager.setRouteForAid(aid, true); 277 } 278 handledAids.add(aid); 279 } 280 // Now, find AIDs in the routing table that are no longer routed to 281 // and remove them. 282 Set<String> routedAids = mRoutingManager.getRoutedAids(); 283 for (String aid : routedAids) { 284 if (!handledAids.contains(aid)) { 285 if (DBG) Log.d(TAG, "Removing routing for AID " + aid + ", because " + 286 "there are no no interested services."); 287 mRoutingManager.removeAid(aid); 288 } 289 } 290 // And commit the routing 291 mRoutingManager.commitRouting(); 292 } 293 294 public void onServicesUpdated(int userId, List<ApduServiceInfo> services) { 295 if (DBG) Log.d(TAG, "onServicesUpdated"); 296 synchronized (mLock) { 297 if (ActivityManager.getCurrentUser() == userId) { 298 // Rebuild our internal data-structures 299 generateAidTreeLocked(services); 300 generateAidCategoriesLocked(services); 301 generateAidCacheLocked(); 302 } else { 303 if (DBG) Log.d(TAG, "Ignoring update because it's not for the current user."); 304 } 305 } 306 } 307 308 public void onPreferredPaymentServiceChanged(ComponentName service) { 309 synchronized (mLock) { 310 mPreferredPaymentService = service; 311 generateAidCacheLocked(); 312 } 313 } 314 315 public void onPreferredForegroundServiceChanged(ComponentName service) { 316 synchronized (mLock) { 317 mPreferredForegroundService = service; 318 generateAidCacheLocked(); 319 } 320 } 321 322 public void onNfcDisabled() { 323 synchronized (mLock) { 324 mNfcEnabled = false; 325 } 326 mRoutingManager.onNfccRoutingTableCleared(); 327 } 328 329 public void onNfcEnabled() { 330 synchronized (mLock) { 331 mNfcEnabled = true; 332 updateRoutingLocked(); 333 } 334 } 335 336 String dumpEntry(Map.Entry<String, AidResolveInfo> entry) { 337 StringBuilder sb = new StringBuilder(); 338 sb.append(" \"" + entry.getKey() + "\"\n"); 339 ApduServiceInfo defaultService = entry.getValue().defaultService; 340 ComponentName defaultComponent = defaultService != null ? 341 defaultService.getComponent() : null; 342 343 for (ApduServiceInfo service : entry.getValue().services) { 344 sb.append(" "); 345 if (service.getComponent().equals(defaultComponent)) { 346 sb.append("*DEFAULT* "); 347 } 348 sb.append(service.getComponent() + 349 " (Description: " + service.getDescription() + ")\n"); 350 } 351 return sb.toString(); 352 } 353 354 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 355 pw.println("AID cache entries: "); 356 for (Map.Entry<String, AidResolveInfo> entry : mAidCache.entrySet()) { 357 pw.println(dumpEntry(entry)); 358 } 359 pw.println(" Service preferred by foreground app: " + mPreferredForegroundService); 360 pw.println(" Preferred payment service: " + mPreferredPaymentService); 361 pw.println(""); 362 mRoutingManager.dump(fd, pw, args); 363 pw.println(""); 364 } 365} 366