RegisteredAidCache.java revision 5b60927762b8512622e5a1d53b93a4b3b49acba2
1package com.android.nfc.cardemulation;
2
3import android.app.ActivityManager;
4import android.content.ComponentName;
5import android.content.Context;
6import android.content.Intent;
7import android.database.ContentObserver;
8import android.net.Uri;
9import android.nfc.cardemulation.ApduServiceInfo;
10import android.nfc.cardemulation.CardEmulation;
11import android.nfc.cardemulation.ApduServiceInfo.AidGroup;
12import android.os.Handler;
13import android.os.Looper;
14import android.os.UserHandle;
15import android.provider.Settings;
16import android.util.Log;
17
18import com.google.android.collect.Maps;
19
20import java.util.ArrayList;
21import java.util.HashMap;
22import java.util.HashSet;
23import java.util.List;
24import java.util.Map;
25import java.util.Set;
26import java.util.SortedMap;
27import java.util.TreeMap;
28
29public class RegisteredAidCache implements RegisteredServicesCache.Callback {
30    static final String TAG = "RegisteredAidCache";
31
32    static final boolean DBG = true;
33
34    // mAidServices is a tree that maps an AID to a list of handling services
35    // on Android. It is only valid for the current user.
36    final TreeMap<String, ArrayList<ApduServiceInfo>> mAidToServices =
37            new TreeMap<String, ArrayList<ApduServiceInfo>>();
38
39    // mAidCache is a lookup table for quickly mapping an AID to one or
40    // more services. It differs from mAidServices in the sense that it
41    // has already accounted for defaults, and hence its return value
42    // is authoritative for the current set of services and defaults.
43    // It is only valid for the current user.
44    final HashMap<String, AidResolveInfo> mAidCache =
45            Maps.newHashMap();
46
47    final HashMap<String, ComponentName> mCategoryDefaults =
48            Maps.newHashMap();
49
50    final class AidResolveInfo {
51        List<ApduServiceInfo> services;
52        ApduServiceInfo defaultService;
53        String aid;
54    }
55
56    /**
57     * AIDs per category
58     */
59    public final HashMap<String, Set<String>> mCategoryAids =
60            Maps.newHashMap();
61
62    final Handler mHandler = new Handler(Looper.getMainLooper());
63    final RegisteredServicesCache mServiceCache;
64
65    final Object mLock = new Object();
66    final Context mContext;
67    final AidRoutingManager mRoutingManager;
68    final SettingsObserver mSettingsObserver;
69
70    ComponentName mNextTapComponent = null;
71    boolean mNfcEnabled = false;
72
73    private final class SettingsObserver extends ContentObserver {
74        public SettingsObserver(Handler handler) {
75            super(handler);
76        }
77
78        @Override
79        public void onChange(boolean selfChange, Uri uri) {
80            super.onChange(selfChange, uri);
81            synchronized (mLock) {
82                // Do it just for the current user. If it was in fact
83                // a change made for another user, we'll sync it down
84                // on user switch.
85                int currentUser = ActivityManager.getCurrentUser();
86                boolean changed = updateFromSettingsLocked(currentUser);
87                if (changed) {
88                    generateAidCacheLocked();
89                    updateRoutingLocked();
90                } else {
91                    if (DBG) Log.d(TAG, "Not updating aid cache + routing: nothing changed.");
92                }
93            }
94        }
95    };
96
97    public RegisteredAidCache(Context context, AidRoutingManager routingManager) {
98        mSettingsObserver = new SettingsObserver(mHandler);
99        mContext = context;
100        mServiceCache = new RegisteredServicesCache(context, this);
101        mRoutingManager = routingManager;
102
103        updateFromSettingsLocked(ActivityManager.getCurrentUser());
104    }
105
106    public boolean isNextTapOverriden() {
107        synchronized (mLock) {
108            return mNextTapComponent != null;
109        }
110    }
111
112    public AidResolveInfo resolveAidPrefix(String aid) {
113        synchronized (mLock) {
114            char nextAidChar = (char) (aid.charAt(aid.length() - 1) + 1);
115            String nextAid = aid.substring(0, aid.length() - 1) + nextAidChar;
116            SortedMap<String, ArrayList<ApduServiceInfo>> matches =
117                    mAidToServices.subMap(aid, nextAid);
118            // The first match is lexicographically closest to what the reader asked;
119            if (matches.isEmpty()) {
120                return null;
121            } else {
122                AidResolveInfo resolveInfo = mAidCache.get(matches.firstKey());
123                // Let the caller know which AID got selected
124                resolveInfo.aid = matches.firstKey();
125                return resolveInfo;
126            }
127        }
128    }
129
130    public String getCategoryForAid(String aid) {
131        synchronized (mLock) {
132            Set<String> paymentAids = mCategoryAids.get(CardEmulation.CATEGORY_PAYMENT);
133            if (paymentAids != null && paymentAids.contains(aid)) {
134                return CardEmulation.CATEGORY_PAYMENT;
135            } else {
136                return CardEmulation.CATEGORY_OTHER;
137            }
138        }
139    }
140
141    public boolean isDefaultServiceForAid(int userId, ComponentName service, String aid) {
142        AidResolveInfo resolveInfo = null;
143        boolean serviceFound = false;
144        synchronized (mLock) {
145            serviceFound = mServiceCache.hasService(userId, service);
146        }
147        if (!serviceFound) {
148            // If we don't know about this service yet, it may have just been enabled
149            // using PackageManager.setComponentEnabledSetting(). The PackageManager
150            // broadcasts are delayed by 10 seconds in that scenario, which causes
151            // calls to our APIs referencing that service to fail.
152            // Hence, update the cache in case we don't know about the service.
153            if (DBG) Log.d(TAG, "Didn't find passed in service, invalidating cache.");
154            mServiceCache.invalidateCache(userId);
155        }
156        synchronized (mLock) {
157            resolveInfo = mAidCache.get(aid);
158        }
159        if (resolveInfo.services == null || resolveInfo.services.size() == 0) return false;
160
161        if (resolveInfo.defaultService != null) {
162            return service.equals(resolveInfo.defaultService.getComponent());
163        } else if (resolveInfo.services.size() == 1) {
164            return service.equals(resolveInfo.services.get(0).getComponent());
165        } else {
166            // More than one service, not the default
167            return false;
168        }
169    }
170
171    public boolean setDefaultServiceForCategory(int userId, ComponentName service,
172            String category) {
173        if (!CardEmulation.CATEGORY_PAYMENT.equals(category)) {
174            Log.e(TAG, "Not allowing defaults for category " + category);
175            return false;
176        }
177        synchronized (mLock) {
178            // TODO Not really nice to be writing to Settings.Secure here...
179            // ideally we overlay our local changes over whatever is in
180            // Settings.Secure
181            if (service == null || mServiceCache.hasService(userId, service)) {
182                Settings.Secure.putStringForUser(mContext.getContentResolver(),
183                        Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT,
184                        service != null ? service.flattenToString() : null, userId);
185            } else {
186                Log.e(TAG, "Could not find default service to make default: " + service);
187            }
188        }
189        return true;
190    }
191
192    public boolean isDefaultServiceForCategory(int userId, String category,
193            ComponentName service) {
194        boolean serviceFound = false;
195        synchronized (mLock) {
196            // If we don't know about this service yet, it may have just been enabled
197            // using PackageManager.setComponentEnabledSetting(). The PackageManager
198            // broadcasts are delayed by 10 seconds in that scenario, which causes
199            // calls to our APIs referencing that service to fail.
200            // Hence, update the cache in case we don't know about the service.
201            serviceFound = mServiceCache.hasService(userId, service);
202        }
203        if (!serviceFound) {
204            if (DBG) Log.d(TAG, "Didn't find passed in service, invalidating cache.");
205            mServiceCache.invalidateCache(userId);
206        }
207        ComponentName defaultService =
208                getDefaultServiceForCategory(userId, category, true);
209        return (defaultService != null && defaultService.equals(service));
210    }
211
212    ComponentName getDefaultServiceForCategory(int userId, String category,
213            boolean validateInstalled) {
214        if (!CardEmulation.CATEGORY_PAYMENT.equals(category)) {
215            Log.e(TAG, "Not allowing defaults for category " + category);
216            return null;
217        }
218        synchronized (mLock) {
219            // Load current payment default from settings
220            String name = Settings.Secure.getStringForUser(
221                    mContext.getContentResolver(), Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT,
222                    userId);
223            if (name != null) {
224                ComponentName service = ComponentName.unflattenFromString(name);
225                if (!validateInstalled || service == null) {
226                    return service;
227                } else {
228                    return mServiceCache.hasService(userId, service) ? service : null;
229                }
230            } else {
231                return null;
232            }
233        }
234    }
235
236    public List<ApduServiceInfo> getServicesForCategory(int userId, String category) {
237        return mServiceCache.getServicesForCategory(userId, category);
238    }
239
240    public boolean setDefaultForNextTap(int userId, ComponentName service) {
241        synchronized (mLock) {
242            if (service != null) {
243                mNextTapComponent = service;
244            } else {
245                mNextTapComponent = null;
246            }
247            // Update cache and routing table
248            generateAidCacheLocked();
249            updateRoutingLocked();
250        }
251        return true;
252    }
253
254    /**
255     * Resolves an AID to a set of services that can handle it.
256     */
257     AidResolveInfo resolveAidLocked(List<ApduServiceInfo> resolvedServices, String aid) {
258        if (resolvedServices == null || resolvedServices.size() == 0) {
259            if (DBG) Log.d(TAG, "Could not resolve AID " + aid + " to any service.");
260            return null;
261        }
262        AidResolveInfo resolveInfo = new AidResolveInfo();
263        Log.e(TAG, "resolveAidLocked: resolving AID " + aid);
264        resolveInfo.services = new ArrayList<ApduServiceInfo>();
265        resolveInfo.services.addAll(resolvedServices);
266        resolveInfo.defaultService = null;
267
268        ComponentName defaultComponent = mNextTapComponent;
269        if (DBG) Log.d(TAG, "resolveAidLocked: next tap component is " + defaultComponent);
270        Set<String> paymentAids = mCategoryAids.get(CardEmulation.CATEGORY_PAYMENT);
271        if (paymentAids != null && paymentAids.contains(aid)) {
272            if (DBG) Log.d(TAG, "resolveAidLocked: AID " + aid + " is a payment AID");
273            // This AID has been registered as a payment AID by at least one service.
274            // Get default component for payment if no next tap default.
275            if (defaultComponent == null) {
276                defaultComponent = mCategoryDefaults.get(CardEmulation.CATEGORY_PAYMENT);
277            }
278            if (DBG) Log.d(TAG, "resolveAidLocked: default payment component is "
279                    + defaultComponent);
280            if (resolvedServices.size() == 1) {
281                ApduServiceInfo resolvedService = resolvedServices.get(0);
282                Log.d(TAG, "resolveAidLocked: resolved single service " +
283                        resolvedService.getComponent());
284                if (defaultComponent != null &&
285                        defaultComponent.equals(resolvedService.getComponent())) {
286                    if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to (default) " +
287                        resolvedService.getComponent());
288                    resolveInfo.defaultService = resolvedService;
289                } else {
290                    // So..since we resolved to only one service, and this AID
291                    // is a payment AID, we know that this service is the only
292                    // service that has registered for this AID and in fact claimed
293                    // it was a payment AID.
294                    // There's two cases:
295                    // 1. All other AIDs in the payment group are uncontended:
296                    //    in this case, just route to this app. It won't get
297                    //    in the way of other apps, and is likely to interact
298                    //    with different terminal infrastructure anyway.
299                    // 2. At least one AID in the payment group is contended:
300                    //    in this case, we should ask the user to confirm,
301                    //    since it is likely to contend with other apps, even
302                    //    when touching the same terminal.
303                    boolean foundConflict = false;
304                    for (AidGroup aidGroup : resolvedService.getAidGroups()) {
305                        if (aidGroup.getCategory().equals(CardEmulation.CATEGORY_PAYMENT)) {
306                            for (String registeredAid : aidGroup.getAids()) {
307                                ArrayList<ApduServiceInfo> servicesForAid =
308                                        mAidToServices.get(registeredAid);
309                                if (servicesForAid != null && servicesForAid.size() > 1) {
310                                    foundConflict = true;
311                                }
312                            }
313                        }
314                    }
315                    if (!foundConflict) {
316                        if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to " +
317                            resolvedService.getComponent());
318                        // Treat this as if it's the default for this AID
319                        resolveInfo.defaultService = resolvedService;
320                    } else {
321                        // Allow this service to handle, but don't set as default
322                        if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing AID " + aid +
323                                " to " + resolvedService.getComponent() +
324                                ", but will ask confirmation because its AID group is contended.");
325                    }
326                }
327            } else if (resolvedServices.size() > 1) {
328                // More services have registered. If there's a default and it
329                // registered this AID, go with the default. Otherwise, add all.
330                if (DBG) Log.d(TAG, "resolveAidLocked: multiple services matched.");
331                if (defaultComponent != null) {
332                    for (ApduServiceInfo service : resolvedServices) {
333                        if (service.getComponent().equals(defaultComponent)) {
334                            if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to (default) "
335                                    + service.getComponent());
336                            resolveInfo.defaultService = service;
337                            break;
338                        }
339                    }
340                    if (resolveInfo.defaultService == null) {
341                        if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to all services");
342                    }
343                }
344            } // else -> should not hit, we checked for 0 before.
345        } else {
346            // This AID is not a payment AID, just return all components
347            // that can handle it, but be mindful of (next tap) defaults.
348            for (ApduServiceInfo service : resolvedServices) {
349                if (service.getComponent().equals(defaultComponent)) {
350                    if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, " +
351                            "routing to (default) " + service.getComponent());
352                    resolveInfo.defaultService = service;
353                    break;
354                }
355            }
356            if (resolveInfo.defaultService == null) {
357                // If we didn't find the default, mark the first as default
358                // if there is only one.
359                if (resolveInfo.services.size() == 1) {
360                    resolveInfo.defaultService = resolveInfo.services.get(0);
361                    if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, " +
362                            "routing to (default) " + resolveInfo.defaultService.getComponent());
363                } else {
364                    if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: cat OTHER AID, routing all");
365                }
366            }
367        }
368        return resolveInfo;
369    }
370
371    void generateAidTreeLocked(List<ApduServiceInfo> services) {
372        // Easiest is to just build the entire tree again
373        mAidToServices.clear();
374        for (ApduServiceInfo service : services) {
375            if (DBG) Log.d(TAG, "generateAidTree component: " + service.getComponent());
376            for (String aid : service.getAids()) {
377                if (DBG) Log.d(TAG, "generateAidTree AID: " + aid);
378                // Check if a mapping exists for this AID
379                if (mAidToServices.containsKey(aid)) {
380                    final ArrayList<ApduServiceInfo> aidServices = mAidToServices.get(aid);
381                    aidServices.add(service);
382                } else {
383                    final ArrayList<ApduServiceInfo> aidServices =
384                            new ArrayList<ApduServiceInfo>();
385                    aidServices.add(service);
386                    mAidToServices.put(aid, aidServices);
387                }
388            }
389        }
390    }
391
392    void generateAidCategoriesLocked(List<ApduServiceInfo> services) {
393        // Trash existing mapping
394        mCategoryAids.clear();
395
396        for (ApduServiceInfo service : services) {
397            ArrayList<AidGroup> aidGroups = service.getAidGroups();
398            if (aidGroups == null) continue;
399            for (AidGroup aidGroup : aidGroups) {
400                String groupCategory = aidGroup.getCategory();
401                Set<String> categoryAids = mCategoryAids.get(groupCategory);
402                if (categoryAids == null) {
403                    categoryAids = new HashSet<String>();
404                }
405                categoryAids.addAll(aidGroup.getAids());
406                mCategoryAids.put(groupCategory, categoryAids);
407            }
408        }
409    }
410
411    boolean updateFromSettingsLocked(int userId) {
412        boolean changed = false;
413
414        // Load current payment default from settings
415        String name = Settings.Secure.getStringForUser(
416                mContext.getContentResolver(), Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT,
417                userId);
418        ComponentName newDefault = name != null ? ComponentName.unflattenFromString(name) : null;
419        ComponentName oldDefault = mCategoryDefaults.put(CardEmulation.CATEGORY_PAYMENT,
420                newDefault);
421        changed |= newDefault != oldDefault;
422        if (DBG) Log.d(TAG, "Updating default component to: " + (name != null ?
423                ComponentName.unflattenFromString(name) : "null"));
424        return changed;
425    }
426
427    void generateAidCacheLocked() {
428        mAidCache.clear();
429        for (Map.Entry<String, ArrayList<ApduServiceInfo>> aidEntry:
430                    mAidToServices.entrySet()) {
431            String aid = aidEntry.getKey();
432            if (!mAidCache.containsKey(aid)) {
433                mAidCache.put(aid, resolveAidLocked(aidEntry.getValue(), aid));
434            }
435        }
436    }
437
438    void updateRoutingLocked() {
439        if (!mNfcEnabled) {
440            if (DBG) Log.d(TAG, "Not updating routing table because NFC is off.");
441            return;
442        }
443        final Set<String> handledAids = new HashSet<String>();
444        // For each AID, find interested services
445        for (Map.Entry<String, AidResolveInfo> aidEntry:
446                mAidCache.entrySet()) {
447            String aid = aidEntry.getKey();
448            AidResolveInfo resolveInfo = aidEntry.getValue();
449            if (resolveInfo.services.size() == 0) {
450                // No interested services, if there is a current routing remove it
451                mRoutingManager.removeAid(aid);
452            } else if (resolveInfo.defaultService != null) {
453                // There is a default service set, route to that service
454                mRoutingManager.setRouteForAid(aid, resolveInfo.defaultService.isOnHost());
455            } else if (resolveInfo.services.size() == 1) {
456                // Only one service, but not the default, must route to host
457                // to ask the user to confirm.
458                mRoutingManager.setRouteForAid(aid, true);
459            } else if (resolveInfo.services.size() > 1) {
460                // Multiple services, need to route to host to ask
461                mRoutingManager.setRouteForAid(aid, true);
462            }
463            handledAids.add(aid);
464        }
465        // Now, find AIDs in the routing table that are no longer routed to
466        // and remove them.
467        Set<String> routedAids = mRoutingManager.getRoutedAids();
468        for (String aid : routedAids) {
469            if (!handledAids.contains(aid)) {
470                if (DBG) Log.d(TAG, "Removing routing for AID " + aid + ", because " +
471                        "there are no no interested services.");
472                mRoutingManager.removeAid(aid);
473            }
474        }
475        // And commit the routing
476        mRoutingManager.commitRouting();
477    }
478
479    void showDefaultRemovedDialog() {
480        Intent intent = new Intent(mContext, DefaultRemovedActivity.class);
481        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
482        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
483    }
484
485    void onPaymentDefaultRemoved(int userId, List<ApduServiceInfo> services) {
486        int numPaymentServices = 0;
487        ComponentName lastFoundPaymentService = null;
488        for (ApduServiceInfo service : services) {
489            if (service.hasCategory(CardEmulation.CATEGORY_PAYMENT))  {
490                numPaymentServices++;
491                lastFoundPaymentService = service.getComponent();
492            }
493        }
494        if (DBG) Log.d(TAG, "Number of payment services is " +
495                Integer.toString(numPaymentServices));
496        if (numPaymentServices == 0) {
497            if (DBG) Log.d(TAG, "Default removed, no services left.");
498            // No payment services left, unset default and don't ask the user
499            setDefaultServiceForCategory(userId, null,
500                    CardEmulation.CATEGORY_PAYMENT);
501        } else if (numPaymentServices == 1) {
502            // Only one left, automatically make it the default
503            if (DBG) Log.d(TAG, "Default removed, making remaining service default.");
504            setDefaultServiceForCategory(userId, lastFoundPaymentService,
505                    CardEmulation.CATEGORY_PAYMENT);
506        } else if (numPaymentServices > 1) {
507            // More than one left, unset default and ask the user if he wants
508            // to set a new one
509            if (DBG) Log.d(TAG, "Default removed, asking user to pick.");
510            setDefaultServiceForCategory(userId, null,
511                    CardEmulation.CATEGORY_PAYMENT);
512            showDefaultRemovedDialog();
513        }
514    }
515
516    void setDefaultIfNeededLocked(int userId, List<ApduServiceInfo> services) {
517        int numPaymentServices = 0;
518        ComponentName lastFoundPaymentService = null;
519        for (ApduServiceInfo service : services) {
520            if (service.hasCategory(CardEmulation.CATEGORY_PAYMENT))  {
521                numPaymentServices++;
522                lastFoundPaymentService = service.getComponent();
523            }
524        }
525        if (numPaymentServices > 1) {
526            // More than one service left, leave default unset
527            if (DBG) Log.d(TAG, "No default set, more than one service left.");
528        } else if (numPaymentServices == 1) {
529            // Make single found payment service the default
530            if (DBG) Log.d(TAG, "No default set, making single service default.");
531            setDefaultServiceForCategory(userId, lastFoundPaymentService,
532                    CardEmulation.CATEGORY_PAYMENT);
533        } else {
534            // No payment services left, leave default at null
535            if (DBG) Log.d(TAG, "No default set, last payment service removed.");
536        }
537    }
538
539    void checkDefaultsLocked(int userId, List<ApduServiceInfo> services) {
540        ComponentName defaultPaymentService =
541                getDefaultServiceForCategory(userId, CardEmulation.CATEGORY_PAYMENT, false);
542        Log.d(TAG, "Current default: " + defaultPaymentService);
543        if (defaultPaymentService != null) {
544            // Validate the default is still installed and handling payment
545            ApduServiceInfo serviceInfo = mServiceCache.getService(userId, defaultPaymentService);
546            if (serviceInfo == null) {
547                Log.e(TAG, "Default payment service unexpectedly removed.");
548                onPaymentDefaultRemoved(userId, services);
549            } else if (!serviceInfo.hasCategory(CardEmulation.CATEGORY_PAYMENT)) {
550                if (DBG) Log.d(TAG, "Default payment service had payment category removed");
551                onPaymentDefaultRemoved(userId, services);
552            } else {
553                // Default still exists and handles the category, nothing do
554                if (DBG) Log.d(TAG, "Default payment service still ok.");
555            }
556        } else {
557            // A payment service may have been removed, leaving only one;
558            // in that case, automatically set that app as default.
559            setDefaultIfNeededLocked(userId, services);
560        }
561    }
562
563    @Override
564    public void onServicesUpdated(int userId, List<ApduServiceInfo> services) {
565        synchronized (mLock) {
566            if (ActivityManager.getCurrentUser() == userId) {
567                // Rebuild our internal data-structures
568                checkDefaultsLocked(userId, services);
569                generateAidTreeLocked(services);
570                generateAidCategoriesLocked(services);
571                generateAidCacheLocked();
572                updateRoutingLocked();
573            } else {
574                if (DBG) Log.d(TAG, "Ignoring update because it's not for the current user.");
575            }
576        }
577    }
578
579    public void invalidateCache(int currentUser) {
580        mServiceCache.invalidateCache(currentUser);
581    }
582
583    public void onNfcDisabled() {
584        synchronized (mLock) {
585            mNfcEnabled = false;
586        }
587        mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
588        mServiceCache.onNfcDisabled();
589        mRoutingManager.onNfccRoutingTableCleared();
590    }
591
592    public void onNfcEnabled() {
593        mContext.getContentResolver().registerContentObserver(
594                Settings.Secure.getUriFor(Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT),
595                true, mSettingsObserver, UserHandle.USER_ALL);
596        synchronized (mLock) {
597            mNfcEnabled = true;
598            updateFromSettingsLocked(ActivityManager.getCurrentUser());
599        }
600        mServiceCache.onNfcEnabled();
601    }
602}
603