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