Home | History | Annotate | Download | only in cardemulation
      1 /*
      2  * Copyright (C) 2013 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 
     17 package com.android.nfc.cardemulation;
     18 
     19 import org.xmlpull.v1.XmlPullParser;
     20 import org.xmlpull.v1.XmlPullParserException;
     21 import org.xmlpull.v1.XmlSerializer;
     22 
     23 import android.app.ActivityManager;
     24 import android.content.BroadcastReceiver;
     25 import android.content.ComponentName;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.IntentFilter;
     29 import android.content.pm.PackageManager;
     30 import android.content.pm.ResolveInfo;
     31 import android.content.pm.ServiceInfo;
     32 import android.content.pm.PackageManager.NameNotFoundException;
     33 import android.nfc.cardemulation.AidGroup;
     34 import android.nfc.cardemulation.ApduServiceInfo;
     35 import android.nfc.cardemulation.CardEmulation;
     36 import android.nfc.cardemulation.HostApduService;
     37 import android.nfc.cardemulation.OffHostApduService;
     38 import android.os.UserHandle;
     39 import android.util.AtomicFile;
     40 import android.util.Log;
     41 import android.util.SparseArray;
     42 import android.util.Xml;
     43 
     44 import com.android.internal.util.FastXmlSerializer;
     45 import com.google.android.collect.Maps;
     46 
     47 import java.io.File;
     48 import java.io.FileDescriptor;
     49 import java.io.FileInputStream;
     50 import java.io.FileOutputStream;
     51 import java.io.IOException;
     52 import java.io.PrintWriter;
     53 import java.util.ArrayList;
     54 import java.util.Collections;
     55 import java.util.HashMap;
     56 import java.util.Iterator;
     57 import java.util.List;
     58 import java.util.Map;
     59 import java.util.concurrent.atomic.AtomicReference;
     60 
     61 /**
     62  * This class is inspired by android.content.pm.RegisteredServicesCache
     63  * That class was not re-used because it doesn't support dynamically
     64  * registering additional properties, but generates everything from
     65  * the manifest. Since we have some properties that are not in the manifest,
     66  * it's less suited.
     67  */
     68 public class RegisteredServicesCache {
     69     static final String XML_INDENT_OUTPUT_FEATURE = "http://xmlpull.org/v1/doc/features.html#indent-output";
     70     static final String TAG = "RegisteredServicesCache";
     71     static final boolean DEBUG = false;
     72 
     73     final Context mContext;
     74     final AtomicReference<BroadcastReceiver> mReceiver;
     75 
     76     final Object mLock = new Object();
     77     // All variables below synchronized on mLock
     78 
     79     // mUserServices holds the card emulation services that are running for each user
     80     final SparseArray<UserServices> mUserServices = new SparseArray<UserServices>();
     81     final Callback mCallback;
     82     final AtomicFile mDynamicAidsFile;
     83 
     84     public interface Callback {
     85         void onServicesUpdated(int userId, final List<ApduServiceInfo> services);
     86     };
     87 
     88     static class DynamicAids {
     89         public final int uid;
     90         public final HashMap<String, AidGroup> aidGroups = Maps.newHashMap();
     91 
     92         DynamicAids(int uid) {
     93             this.uid = uid;
     94         }
     95     };
     96 
     97     private static class UserServices {
     98         /**
     99          * All services that have registered
    100          */
    101         final HashMap<ComponentName, ApduServiceInfo> services =
    102                 Maps.newHashMap(); // Re-built at run-time
    103         final HashMap<ComponentName, DynamicAids> dynamicAids =
    104                 Maps.newHashMap(); // In memory cache of dynamic AID store
    105     };
    106 
    107     private UserServices findOrCreateUserLocked(int userId) {
    108         UserServices services = mUserServices.get(userId);
    109         if (services == null) {
    110             services = new UserServices();
    111             mUserServices.put(userId, services);
    112         }
    113         return services;
    114     }
    115 
    116     public RegisteredServicesCache(Context context, Callback callback) {
    117         mContext = context;
    118         mCallback = callback;
    119 
    120         final BroadcastReceiver receiver = new BroadcastReceiver() {
    121             @Override
    122             public void onReceive(Context context, Intent intent) {
    123                 final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
    124                 String action = intent.getAction();
    125                 if (DEBUG) Log.d(TAG, "Intent action: " + action);
    126                 if (uid != -1) {
    127                     boolean replaced = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) &&
    128                             (Intent.ACTION_PACKAGE_ADDED.equals(action) ||
    129                              Intent.ACTION_PACKAGE_REMOVED.equals(action));
    130                     if (!replaced) {
    131                         int currentUser = ActivityManager.getCurrentUser();
    132                         if (currentUser == UserHandle.getUserId(uid)) {
    133                             invalidateCache(UserHandle.getUserId(uid));
    134                         } else {
    135                             // Cache will automatically be updated on user switch
    136                         }
    137                     } else {
    138                         if (DEBUG) Log.d(TAG, "Ignoring package intent due to package being replaced.");
    139                     }
    140                 }
    141             }
    142         };
    143         mReceiver = new AtomicReference<BroadcastReceiver>(receiver);
    144 
    145         IntentFilter intentFilter = new IntentFilter();
    146         intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
    147         intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
    148         intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
    149         intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
    150         intentFilter.addAction(Intent.ACTION_PACKAGE_FIRST_LAUNCH);
    151         intentFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
    152         intentFilter.addDataScheme("package");
    153         mContext.registerReceiverAsUser(mReceiver.get(), UserHandle.ALL, intentFilter, null, null);
    154 
    155         // Register for events related to sdcard operations
    156         IntentFilter sdFilter = new IntentFilter();
    157         sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
    158         sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
    159         mContext.registerReceiverAsUser(mReceiver.get(), UserHandle.ALL, sdFilter, null, null);
    160 
    161         File dataDir = mContext.getFilesDir();
    162         mDynamicAidsFile = new AtomicFile(new File(dataDir, "dynamic_aids.xml"));
    163     }
    164 
    165     void initialize() {
    166         synchronized (mLock) {
    167             readDynamicAidsLocked();
    168         }
    169         invalidateCache(ActivityManager.getCurrentUser());
    170     }
    171 
    172     void dump(ArrayList<ApduServiceInfo> services) {
    173         for (ApduServiceInfo service : services) {
    174             if (DEBUG) Log.d(TAG, service.toString());
    175         }
    176     }
    177 
    178     boolean containsServiceLocked(ArrayList<ApduServiceInfo> services, ComponentName serviceName) {
    179         for (ApduServiceInfo service : services) {
    180             if (service.getComponent().equals(serviceName)) return true;
    181         }
    182         return false;
    183     }
    184 
    185     public boolean hasService(int userId, ComponentName service) {
    186         return getService(userId, service) != null;
    187     }
    188 
    189     public ApduServiceInfo getService(int userId, ComponentName service) {
    190         synchronized (mLock) {
    191             UserServices userServices = findOrCreateUserLocked(userId);
    192             return userServices.services.get(service);
    193         }
    194     }
    195 
    196     public List<ApduServiceInfo> getServices(int userId) {
    197         final ArrayList<ApduServiceInfo> services = new ArrayList<ApduServiceInfo>();
    198         synchronized (mLock) {
    199             UserServices userServices = findOrCreateUserLocked(userId);
    200             services.addAll(userServices.services.values());
    201         }
    202         return services;
    203     }
    204 
    205     public List<ApduServiceInfo> getServicesForCategory(int userId, String category) {
    206         final ArrayList<ApduServiceInfo> services = new ArrayList<ApduServiceInfo>();
    207         synchronized (mLock) {
    208             UserServices userServices = findOrCreateUserLocked(userId);
    209             for (ApduServiceInfo service : userServices.services.values()) {
    210                 if (service.hasCategory(category)) services.add(service);
    211             }
    212         }
    213         return services;
    214     }
    215 
    216     ArrayList<ApduServiceInfo> getInstalledServices(int userId) {
    217         PackageManager pm;
    218         try {
    219             pm = mContext.createPackageContextAsUser("android", 0,
    220                     new UserHandle(userId)).getPackageManager();
    221         } catch (NameNotFoundException e) {
    222             Log.e(TAG, "Could not create user package context");
    223             return null;
    224         }
    225 
    226         ArrayList<ApduServiceInfo> validServices = new ArrayList<ApduServiceInfo>();
    227 
    228         List<ResolveInfo> resolvedServices = pm.queryIntentServicesAsUser(
    229                 new Intent(HostApduService.SERVICE_INTERFACE),
    230                 PackageManager.GET_META_DATA, userId);
    231 
    232         List<ResolveInfo> resolvedOffHostServices = pm.queryIntentServicesAsUser(
    233                 new Intent(OffHostApduService.SERVICE_INTERFACE),
    234                 PackageManager.GET_META_DATA, userId);
    235         resolvedServices.addAll(resolvedOffHostServices);
    236 
    237         for (ResolveInfo resolvedService : resolvedServices) {
    238             try {
    239                 boolean onHost = !resolvedOffHostServices.contains(resolvedService);
    240                 ServiceInfo si = resolvedService.serviceInfo;
    241                 ComponentName componentName = new ComponentName(si.packageName, si.name);
    242                 // Check if the package holds the NFC permission
    243                 if (pm.checkPermission(android.Manifest.permission.NFC, si.packageName) !=
    244                         PackageManager.PERMISSION_GRANTED) {
    245                     Log.e(TAG, "Skipping APDU service " + componentName +
    246                             ": it does not require the permission " +
    247                             android.Manifest.permission.NFC);
    248                     continue;
    249                 }
    250                 if (!android.Manifest.permission.BIND_NFC_SERVICE.equals(
    251                         si.permission)) {
    252                     Log.e(TAG, "Skipping APDU service " + componentName +
    253                             ": it does not require the permission " +
    254                             android.Manifest.permission.BIND_NFC_SERVICE);
    255                     continue;
    256                 }
    257                 ApduServiceInfo service = new ApduServiceInfo(pm, resolvedService, onHost);
    258                 if (service != null) {
    259                     validServices.add(service);
    260                 }
    261             } catch (XmlPullParserException e) {
    262                 Log.w(TAG, "Unable to load component info " + resolvedService.toString(), e);
    263             } catch (IOException e) {
    264                 Log.w(TAG, "Unable to load component info " + resolvedService.toString(), e);
    265             }
    266         }
    267 
    268         return validServices;
    269     }
    270 
    271     public void invalidateCache(int userId) {
    272         final ArrayList<ApduServiceInfo> validServices = getInstalledServices(userId);
    273         if (validServices == null) {
    274             return;
    275         }
    276         synchronized (mLock) {
    277             UserServices userServices = findOrCreateUserLocked(userId);
    278 
    279             // Find removed services
    280             Iterator<Map.Entry<ComponentName, ApduServiceInfo>> it =
    281                     userServices.services.entrySet().iterator();
    282             while (it.hasNext()) {
    283                 Map.Entry<ComponentName, ApduServiceInfo> entry =
    284                         (Map.Entry<ComponentName, ApduServiceInfo>) it.next();
    285                 if (!containsServiceLocked(validServices, entry.getKey())) {
    286                     Log.d(TAG, "Service removed: " + entry.getKey());
    287                     it.remove();
    288                 }
    289             }
    290             for (ApduServiceInfo service : validServices) {
    291                 if (DEBUG) Log.d(TAG, "Adding service: " + service.getComponent() +
    292                         " AIDs: " + service.getAids());
    293                 userServices.services.put(service.getComponent(), service);
    294             }
    295 
    296             // Apply dynamic AID mappings
    297             ArrayList<ComponentName> toBeRemoved = new ArrayList<ComponentName>();
    298             for (Map.Entry<ComponentName, DynamicAids> entry :
    299                     userServices.dynamicAids.entrySet()) {
    300                 // Verify component / uid match
    301                 ComponentName component = entry.getKey();
    302                 DynamicAids dynamicAids = entry.getValue();
    303                 ApduServiceInfo serviceInfo = userServices.services.get(component);
    304                 if (serviceInfo == null || (serviceInfo.getUid() != dynamicAids.uid)) {
    305                     toBeRemoved.add(component);
    306                     continue;
    307                 } else {
    308                     for (AidGroup group : dynamicAids.aidGroups.values()) {
    309                         serviceInfo.setOrReplaceDynamicAidGroup(group);
    310                     }
    311                 }
    312             }
    313 
    314             if (toBeRemoved.size() > 0) {
    315                 for (ComponentName component : toBeRemoved) {
    316                     Log.d(TAG, "Removing dynamic AIDs registered by " + component);
    317                     userServices.dynamicAids.remove(component);
    318                 }
    319                 // Persist to filesystem
    320                 writeDynamicAidsLocked();
    321             }
    322         }
    323 
    324         mCallback.onServicesUpdated(userId, Collections.unmodifiableList(validServices));
    325         dump(validServices);
    326     }
    327 
    328     private void readDynamicAidsLocked() {
    329         FileInputStream fis = null;
    330         try {
    331             if (!mDynamicAidsFile.getBaseFile().exists()) {
    332                 Log.d(TAG, "Dynamic AIDs file does not exist.");
    333                 return;
    334             }
    335             fis = mDynamicAidsFile.openRead();
    336             XmlPullParser parser = Xml.newPullParser();
    337             parser.setInput(fis, null);
    338             int eventType = parser.getEventType();
    339             while (eventType != XmlPullParser.START_TAG &&
    340                     eventType != XmlPullParser.END_DOCUMENT) {
    341                 eventType = parser.next();
    342             }
    343             String tagName = parser.getName();
    344             if ("services".equals(tagName)) {
    345                 boolean inService = false;
    346                 ComponentName currentComponent = null;
    347                 int currentUid = -1;
    348                 ArrayList<AidGroup> currentGroups = new ArrayList<AidGroup>();
    349                 while (eventType != XmlPullParser.END_DOCUMENT) {
    350                     tagName = parser.getName();
    351                     if (eventType == XmlPullParser.START_TAG) {
    352                         if ("service".equals(tagName) && parser.getDepth() == 2) {
    353                             String compString = parser.getAttributeValue(null, "component");
    354                             String uidString = parser.getAttributeValue(null, "uid");
    355                             if (compString == null || uidString == null) {
    356                                 Log.e(TAG, "Invalid service attributes");
    357                             } else {
    358                                 try {
    359                                     currentUid = Integer.parseInt(uidString);
    360                                     currentComponent = ComponentName.unflattenFromString(compString);
    361                                     inService = true;
    362                                 } catch (NumberFormatException e) {
    363                                     Log.e(TAG, "Could not parse service uid");
    364                                 }
    365                             }
    366                         }
    367                         if ("aid-group".equals(tagName) && parser.getDepth() == 3 && inService) {
    368                             AidGroup group = AidGroup.createFromXml(parser);
    369                             if (group != null) {
    370                                 currentGroups.add(group);
    371                             } else {
    372                                 Log.e(TAG, "Could not parse AID group.");
    373                             }
    374                         }
    375                     } else if (eventType == XmlPullParser.END_TAG) {
    376                         if ("service".equals(tagName)) {
    377                             // See if we have a valid service
    378                             if (currentComponent != null && currentUid >= 0 &&
    379                                     currentGroups.size() > 0) {
    380                                 final int userId = UserHandle.getUserId(currentUid);
    381                                 DynamicAids dynAids = new DynamicAids(currentUid);
    382                                 for (AidGroup group : currentGroups) {
    383                                     dynAids.aidGroups.put(group.getCategory(), group);
    384                                 }
    385                                 UserServices services = findOrCreateUserLocked(userId);
    386                                 services.dynamicAids.put(currentComponent, dynAids);
    387                             }
    388                             currentUid = -1;
    389                             currentComponent = null;
    390                             currentGroups.clear();
    391                             inService = false;
    392                         }
    393                     }
    394                     eventType = parser.next();
    395                 };
    396             }
    397         } catch (Exception e) {
    398             Log.e(TAG, "Could not parse dynamic AIDs file, trashing.");
    399             mDynamicAidsFile.delete();
    400         } finally {
    401             if (fis != null) {
    402                 try {
    403                     fis.close();
    404                 } catch (IOException e) {
    405                 }
    406             }
    407         }
    408     }
    409 
    410     private boolean writeDynamicAidsLocked() {
    411         FileOutputStream fos = null;
    412         try {
    413             fos = mDynamicAidsFile.startWrite();
    414             XmlSerializer out = new FastXmlSerializer();
    415             out.setOutput(fos, "utf-8");
    416             out.startDocument(null, true);
    417             out.setFeature(XML_INDENT_OUTPUT_FEATURE, true);
    418             out.startTag(null, "services");
    419             for (int i = 0; i < mUserServices.size(); i++) {
    420                 final UserServices user = mUserServices.valueAt(i);
    421                 for (Map.Entry<ComponentName, DynamicAids> service : user.dynamicAids.entrySet()) {
    422                     out.startTag(null, "service");
    423                     out.attribute(null, "component", service.getKey().flattenToString());
    424                     out.attribute(null, "uid", Integer.toString(service.getValue().uid));
    425                     for (AidGroup group : service.getValue().aidGroups.values()) {
    426                         group.writeAsXml(out);
    427                     }
    428                     out.endTag(null, "service");
    429                 }
    430             }
    431             out.endTag(null, "services");
    432             out.endDocument();
    433             mDynamicAidsFile.finishWrite(fos);
    434             return true;
    435         } catch (Exception e) {
    436             Log.e(TAG, "Error writing dynamic AIDs", e);
    437             if (fos != null) {
    438                 mDynamicAidsFile.failWrite(fos);
    439             }
    440             return false;
    441         }
    442     }
    443 
    444     public boolean registerAidGroupForService(int userId, int uid,
    445             ComponentName componentName, AidGroup aidGroup) {
    446         ArrayList<ApduServiceInfo> newServices = null;
    447         boolean success;
    448         synchronized (mLock) {
    449             UserServices services = findOrCreateUserLocked(userId);
    450             // Check if we can find this service
    451             ApduServiceInfo serviceInfo = getService(userId, componentName);
    452             if (serviceInfo == null) {
    453                 Log.e(TAG, "Service " + componentName + " does not exist.");
    454                 return false;
    455             }
    456             if (serviceInfo.getUid() != uid) {
    457                 // This is probably a good indication something is wrong here.
    458                 // Either newer service installed with different uid (but then
    459                 // we should have known about it), or somebody calling us from
    460                 // a different uid.
    461                 Log.e(TAG, "UID mismatch.");
    462                 return false;
    463             }
    464             // Do another AID validation, since a caller could have thrown in a modified
    465             // AidGroup object with invalid AIDs over Binder.
    466             List<String> aids = aidGroup.getAids();
    467             for (String aid : aids) {
    468                 if (!CardEmulation.isValidAid(aid)) {
    469                     Log.e(TAG, "AID " + aid + " is not a valid AID");
    470                     return false;
    471                 }
    472             }
    473             serviceInfo.setOrReplaceDynamicAidGroup(aidGroup);
    474             DynamicAids dynAids = services.dynamicAids.get(componentName);
    475             if (dynAids == null) {
    476                 dynAids = new DynamicAids(uid);
    477                 services.dynamicAids.put(componentName, dynAids);
    478             }
    479             dynAids.aidGroups.put(aidGroup.getCategory(), aidGroup);
    480             success = writeDynamicAidsLocked();
    481             if (success) {
    482                 newServices = new ArrayList<ApduServiceInfo>(services.services.values());
    483             } else {
    484                 Log.e(TAG, "Failed to persist AID group.");
    485                 // Undo registration
    486                 dynAids.aidGroups.remove(aidGroup.getCategory());
    487             }
    488         }
    489         if (success) {
    490             // Make callback without the lock held
    491             mCallback.onServicesUpdated(userId, newServices);
    492         }
    493         return success;
    494     }
    495 
    496     public AidGroup getAidGroupForService(int userId, int uid, ComponentName componentName,
    497             String category) {
    498         ApduServiceInfo serviceInfo = getService(userId, componentName);
    499         if (serviceInfo != null) {
    500             if (serviceInfo.getUid() != uid) {
    501                 Log.e(TAG, "UID mismatch");
    502                 return null;
    503             }
    504             return serviceInfo.getDynamicAidGroupForCategory(category);
    505         } else {
    506             Log.e(TAG, "Could not find service " + componentName);
    507             return null;
    508         }
    509     }
    510 
    511     public boolean removeAidGroupForService(int userId, int uid, ComponentName componentName,
    512             String category) {
    513         boolean success = false;
    514         ArrayList<ApduServiceInfo> newServices = null;
    515         synchronized (mLock) {
    516             UserServices services = findOrCreateUserLocked(userId);
    517             ApduServiceInfo serviceInfo = getService(userId, componentName);
    518             if (serviceInfo != null) {
    519                 if (serviceInfo.getUid() != uid) {
    520                     // Calling from different uid
    521                     Log.e(TAG, "UID mismatch");
    522                     return false;
    523                 }
    524                 if (!serviceInfo.removeDynamicAidGroupForCategory(category)) {
    525                     Log.e(TAG," Could not find dynamic AIDs for category " + category);
    526                     return false;
    527                 }
    528                 // Remove from local cache
    529                 DynamicAids dynAids = services.dynamicAids.get(componentName);
    530                 if (dynAids != null) {
    531                     AidGroup deletedGroup = dynAids.aidGroups.remove(category);
    532                     success = writeDynamicAidsLocked();
    533                     if (success) {
    534                         newServices = new ArrayList<ApduServiceInfo>(services.services.values());
    535                     } else {
    536                         Log.e(TAG, "Could not persist deleted AID group.");
    537                         dynAids.aidGroups.put(category, deletedGroup);
    538                         return false;
    539                     }
    540                 } else {
    541                     Log.e(TAG, "Could not find aid group in local cache.");
    542                 }
    543             } else {
    544                 Log.e(TAG, "Service " + componentName + " does not exist.");
    545             }
    546         }
    547         if (success) {
    548             mCallback.onServicesUpdated(userId, newServices);
    549         }
    550         return success;
    551     }
    552 
    553     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    554         pw.println("Registered HCE services for current user: ");
    555         UserServices userServices = findOrCreateUserLocked(ActivityManager.getCurrentUser());
    556         for (ApduServiceInfo service : userServices.services.values()) {
    557             service.dump(fd, pw, args);
    558             pw.println("");
    559         }
    560         pw.println("");
    561     }
    562 
    563 }
    564