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