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