/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.nfc.cardemulation; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.nfc.cardemulation.NfcFServiceInfo; import android.nfc.cardemulation.NfcFCardEmulation; import android.nfc.cardemulation.HostNfcFService; import android.os.UserHandle; import android.util.AtomicFile; import android.util.Log; import android.util.SparseArray; import android.util.Xml; import com.android.internal.util.FastXmlSerializer; import com.google.android.collect.Maps; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; public class RegisteredNfcFServicesCache { static final String XML_INDENT_OUTPUT_FEATURE = "http://xmlpull.org/v1/doc/features.html#indent-output"; static final String TAG = "RegisteredNfcFServicesCache"; static final boolean DBG = false; final Context mContext; final AtomicReference mReceiver; final Object mLock = new Object(); // All variables below synchronized on mLock // mUserServices holds the card emulation services that are running for each user final SparseArray mUserServices = new SparseArray(); final Callback mCallback; final AtomicFile mDynamicSystemCodeNfcid2File; boolean mActivated = false; public interface Callback { void onNfcFServicesUpdated(int userId, final List services); }; static class DynamicSystemCode { public final int uid; public final String systemCode; DynamicSystemCode(int uid, String systemCode) { this.uid = uid; this.systemCode = systemCode; } }; static class DynamicNfcid2 { public final int uid; public final String nfcid2; DynamicNfcid2(int uid, String nfcid2) { this.uid = uid; this.nfcid2 = nfcid2; } }; private static class UserServices { /** * All services that have registered */ final HashMap services = Maps.newHashMap(); // Re-built at run-time final HashMap dynamicSystemCode = Maps.newHashMap(); // In memory cache of dynamic System Code store final HashMap dynamicNfcid2 = Maps.newHashMap(); // In memory cache of dynamic NFCID2 store }; private UserServices findOrCreateUserLocked(int userId) { UserServices userServices = mUserServices.get(userId); if (userServices == null) { userServices = new UserServices(); mUserServices.put(userId, userServices); } return userServices; } public RegisteredNfcFServicesCache(Context context, Callback callback) { mContext = context; mCallback = callback; final BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); String action = intent.getAction(); if (DBG) Log.d(TAG, "Intent action: " + action); if (uid != -1) { boolean replaced = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) && (Intent.ACTION_PACKAGE_ADDED.equals(action) || Intent.ACTION_PACKAGE_REMOVED.equals(action)); if (!replaced) { int currentUser = ActivityManager.getCurrentUser(); if (currentUser == UserHandle.getUserId(uid)) { invalidateCache(UserHandle.getUserId(uid)); } else { // Cache will automatically be updated on user switch } } else { if (DBG) Log.d(TAG, "Ignoring package intent due to package being replaced."); } } } }; mReceiver = new AtomicReference(receiver); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); intentFilter.addAction(Intent.ACTION_PACKAGE_FIRST_LAUNCH); intentFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED); intentFilter.addDataScheme("package"); mContext.registerReceiverAsUser(mReceiver.get(), UserHandle.ALL, intentFilter, null, null); // Register for events related to sdcard operations IntentFilter sdFilter = new IntentFilter(); sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); mContext.registerReceiverAsUser(mReceiver.get(), UserHandle.ALL, sdFilter, null, null); File dataDir = mContext.getFilesDir(); mDynamicSystemCodeNfcid2File = new AtomicFile(new File(dataDir, "dynamic_systemcode_nfcid2.xml")); } void initialize() { synchronized (mLock) { readDynamicSystemCodeNfcid2Locked(); } invalidateCache(ActivityManager.getCurrentUser()); } void dump(ArrayList services) { for (NfcFServiceInfo service : services) { Log.d(TAG, service.toString()); } } boolean containsServiceLocked(ArrayList services, ComponentName componentName) { for (NfcFServiceInfo service : services) { if (service.getComponent().equals(componentName)) return true; } return false; } public boolean hasService(int userId, ComponentName componentName) { return getService(userId, componentName) != null; } public NfcFServiceInfo getService(int userId, ComponentName componentName) { synchronized (mLock) { UserServices userServices = findOrCreateUserLocked(userId); return userServices.services.get(componentName); } } public List getServices(int userId) { final ArrayList services = new ArrayList(); synchronized (mLock) { UserServices userServices = findOrCreateUserLocked(userId); services.addAll(userServices.services.values()); } return services; } ArrayList getInstalledServices(int userId) { if (DBG) Log.d(TAG, "getInstalledServices"); PackageManager pm; try { pm = mContext.createPackageContextAsUser("android", 0, new UserHandle(userId)).getPackageManager(); } catch (NameNotFoundException e) { Log.e(TAG, "Could not create user package context"); return null; } ArrayList validServices = new ArrayList(); List resolvedServices = pm.queryIntentServicesAsUser( new Intent(HostNfcFService.SERVICE_INTERFACE), PackageManager.GET_META_DATA, userId); for (ResolveInfo resolvedService : resolvedServices) { try { ServiceInfo si = resolvedService.serviceInfo; ComponentName componentName = new ComponentName(si.packageName, si.name); // Check if the package holds the NFC permission if (pm.checkPermission(android.Manifest.permission.NFC, si.packageName) != PackageManager.PERMISSION_GRANTED) { Log.e(TAG, "Skipping NfcF service " + componentName + ": it does not require the permission " + android.Manifest.permission.NFC); continue; } if (!android.Manifest.permission.BIND_NFC_SERVICE.equals( si.permission)) { Log.e(TAG, "Skipping NfcF service " + componentName + ": it does not require the permission " + android.Manifest.permission.BIND_NFC_SERVICE); continue; } NfcFServiceInfo service = new NfcFServiceInfo(pm, resolvedService); if (service != null) { validServices.add(service); } } catch (XmlPullParserException e) { Log.w(TAG, "Unable to load component info " + resolvedService.toString(), e); } catch (IOException e) { Log.w(TAG, "Unable to load component info " + resolvedService.toString(), e); } } return validServices; } public void invalidateCache(int userId) { if (DBG) Log.d(TAG, "invalidateCache"); final ArrayList validServices = getInstalledServices(userId); if (validServices == null) { return; } ArrayList newServices = null; synchronized (mLock) { UserServices userServices = findOrCreateUserLocked(userId); // Check update ArrayList cachedServices = new ArrayList(userServices.services.values()); ArrayList toBeAdded = new ArrayList(); ArrayList toBeRemoved = new ArrayList(); boolean matched = false; for (NfcFServiceInfo validService : validServices) { for (NfcFServiceInfo cachedService : cachedServices) { if (validService.equals(cachedService)) { matched = true; break; } } if (!matched) { toBeAdded.add(validService); } matched = false; } for (NfcFServiceInfo cachedService : cachedServices) { for (NfcFServiceInfo validService : validServices) { if (cachedService.equals(validService)) { matched = true; break; } } if (!matched) { toBeRemoved.add(cachedService); } matched = false; } if (toBeAdded.size() == 0 && toBeRemoved.size() == 0) { Log.d(TAG, "Service unchanged, not updating"); return; } // Update cache for (NfcFServiceInfo service : toBeAdded) { userServices.services.put(service.getComponent(), service); if (DBG) Log.d(TAG, "Added service: " + service.getComponent()); } for (NfcFServiceInfo service : toBeRemoved) { userServices.services.remove(service.getComponent()); if (DBG) Log.d(TAG, "Removed service: " + service.getComponent()); } // Apply dynamic System Code mappings ArrayList toBeRemovedDynamicSystemCode = new ArrayList(); for (Map.Entry entry : userServices.dynamicSystemCode.entrySet()) { // Verify component / uid match ComponentName componentName = entry.getKey(); DynamicSystemCode dynamicSystemCode = entry.getValue(); NfcFServiceInfo service = userServices.services.get(componentName); if (service == null || (service.getUid() != dynamicSystemCode.uid)) { toBeRemovedDynamicSystemCode.add(componentName); continue; } else { service.setOrReplaceDynamicSystemCode(dynamicSystemCode.systemCode); } } // Apply dynamic NFCID2 mappings ArrayList toBeRemovedDynamicNfcid2 = new ArrayList(); for (Map.Entry entry : userServices.dynamicNfcid2.entrySet()) { // Verify component / uid match ComponentName componentName = entry.getKey(); DynamicNfcid2 dynamicNfcid2 = entry.getValue(); NfcFServiceInfo service = userServices.services.get(componentName); if (service == null || (service.getUid() != dynamicNfcid2.uid)) { toBeRemovedDynamicNfcid2.add(componentName); continue; } else { service.setOrReplaceDynamicNfcid2(dynamicNfcid2.nfcid2); } } for (ComponentName removedComponent : toBeRemovedDynamicSystemCode) { Log.d(TAG, "Removing dynamic System Code registered by " + removedComponent); userServices.dynamicSystemCode.remove(removedComponent); } for (ComponentName removedComponent : toBeRemovedDynamicNfcid2) { Log.d(TAG, "Removing dynamic NFCID2 registered by " + removedComponent); userServices.dynamicNfcid2.remove(removedComponent); } // Assign a NFCID2 for services requesting a random NFCID2, then apply boolean nfcid2Assigned = false; for (Map.Entry entry : userServices.services.entrySet()) { NfcFServiceInfo service = entry.getValue(); if (service.getNfcid2().equalsIgnoreCase("RANDOM")) { String randomNfcid2 = generateRandomNfcid2(); service.setOrReplaceDynamicNfcid2(randomNfcid2); DynamicNfcid2 dynamicNfcid2 = new DynamicNfcid2(service.getUid(), randomNfcid2); userServices.dynamicNfcid2.put(entry.getKey(), dynamicNfcid2); nfcid2Assigned = true; } } // Persist to filesystem if (toBeRemovedDynamicSystemCode.size() > 0 || toBeRemovedDynamicNfcid2.size() > 0 || nfcid2Assigned) { writeDynamicSystemCodeNfcid2Locked(); } newServices = new ArrayList(userServices.services.values()); } mCallback.onNfcFServicesUpdated(userId, Collections.unmodifiableList(newServices)); if (DBG) dump(newServices); } private void readDynamicSystemCodeNfcid2Locked() { if (DBG) Log.d(TAG, "readDynamicSystemCodeNfcid2Locked"); FileInputStream fis = null; try { if (!mDynamicSystemCodeNfcid2File.getBaseFile().exists()) { Log.d(TAG, "Dynamic System Code, NFCID2 file does not exist."); return; } fis = mDynamicSystemCodeNfcid2File.openRead(); XmlPullParser parser = Xml.newPullParser(); parser.setInput(fis, null); int eventType = parser.getEventType(); while (eventType != XmlPullParser.START_TAG && eventType != XmlPullParser.END_DOCUMENT) { eventType = parser.next(); } String tagName = parser.getName(); if ("services".equals(tagName)) { ComponentName componentName = null; int currentUid = -1; String systemCode = null; String nfcid2 = null; String description = null; while (eventType != XmlPullParser.END_DOCUMENT) { tagName = parser.getName(); if (eventType == XmlPullParser.START_TAG) { if ("service".equals(tagName) && parser.getDepth() == 2) { String compString = parser.getAttributeValue(null, "component"); String uidString = parser.getAttributeValue(null, "uid"); String systemCodeString = parser.getAttributeValue(null, "system-code"); String descriptionString = parser.getAttributeValue(null, "description"); String nfcid2String = parser.getAttributeValue(null, "nfcid2"); if (compString == null || uidString == null) { Log.e(TAG, "Invalid service attributes"); } else { try { componentName = ComponentName.unflattenFromString(compString); currentUid = Integer.parseInt(uidString); systemCode = systemCodeString; description = descriptionString; nfcid2 = nfcid2String; } catch (NumberFormatException e) { Log.e(TAG, "Could not parse service uid"); } } } } else if (eventType == XmlPullParser.END_TAG) { if ("service".equals(tagName)) { // See if we have a valid service if (componentName != null && currentUid >= 0) { final int userId = UserHandle.getUserId(currentUid); UserServices userServices = findOrCreateUserLocked(userId); if (systemCode != null) { DynamicSystemCode dynamicSystemCode = new DynamicSystemCode(currentUid, systemCode); userServices.dynamicSystemCode.put( componentName, dynamicSystemCode); } if (nfcid2 != null) { DynamicNfcid2 dynamicNfcid2 = new DynamicNfcid2(currentUid, nfcid2); userServices.dynamicNfcid2.put( componentName, dynamicNfcid2); } } componentName = null; currentUid = -1; systemCode = null; description = null; nfcid2 = null; } } eventType = parser.next(); }; } } catch (Exception e) { Log.e(TAG, "Could not parse dynamic System Code, NFCID2 file, trashing."); mDynamicSystemCodeNfcid2File.delete(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { } } } } private boolean writeDynamicSystemCodeNfcid2Locked() { if (DBG) Log.d(TAG, "writeDynamicSystemCodeNfcid2Locked"); FileOutputStream fos = null; try { fos = mDynamicSystemCodeNfcid2File.startWrite(); XmlSerializer out = new FastXmlSerializer(); out.setOutput(fos, "utf-8"); out.startDocument(null, true); out.setFeature(XML_INDENT_OUTPUT_FEATURE, true); out.startTag(null, "services"); for (int i = 0; i < mUserServices.size(); i++) { final UserServices userServices = mUserServices.valueAt(i); for (Map.Entry entry : userServices.dynamicSystemCode.entrySet()) { out.startTag(null, "service"); out.attribute(null, "component", entry.getKey().flattenToString()); out.attribute(null, "uid", Integer.toString(entry.getValue().uid)); out.attribute(null, "system-code", entry.getValue().systemCode); if (userServices.dynamicNfcid2.containsKey(entry.getKey())) { out.attribute(null, "nfcid2", userServices.dynamicNfcid2.get(entry.getKey()).nfcid2); } out.endTag(null, "service"); } for (Map.Entry entry : userServices.dynamicNfcid2.entrySet()) { if (!userServices.dynamicSystemCode.containsKey(entry.getKey())) { out.startTag(null, "service"); out.attribute(null, "component", entry.getKey().flattenToString()); out.attribute(null, "uid", Integer.toString(entry.getValue().uid)); out.attribute(null, "nfcid2", entry.getValue().nfcid2); out.endTag(null, "service"); } } } out.endTag(null, "services"); out.endDocument(); mDynamicSystemCodeNfcid2File.finishWrite(fos); return true; } catch (Exception e) { Log.e(TAG, "Error writing dynamic System Code, NFCID2", e); if (fos != null) { mDynamicSystemCodeNfcid2File.failWrite(fos); } return false; } } public boolean registerSystemCodeForService(int userId, int uid, ComponentName componentName, String systemCode) { if (DBG) Log.d(TAG, "registerSystemCodeForService"); ArrayList newServices = null; boolean success; synchronized (mLock) { if (mActivated) { Log.d(TAG, "failed to register System Code during activation"); return false; } UserServices userServices = findOrCreateUserLocked(userId); // Check if we can find this service NfcFServiceInfo service = getService(userId, componentName); if (service == null) { Log.e(TAG, "Service " + componentName + " does not exist."); return false; } if (service.getUid() != uid) { // This is probably a good indication something is wrong here. // Either newer service installed with different uid (but then // we should have known about it), or somebody calling us from // a different uid. Log.e(TAG, "UID mismatch."); return false; } if (!systemCode.equalsIgnoreCase("NULL") && !NfcFCardEmulation.isValidSystemCode(systemCode)) { Log.e(TAG, "System Code " + systemCode + " is not a valid System Code"); return false; } // Apply dynamic System Code mappings systemCode = systemCode.toUpperCase(); DynamicSystemCode oldDynamicSystemCode = userServices.dynamicSystemCode.get(componentName); DynamicSystemCode dynamicSystemCode = new DynamicSystemCode(uid, systemCode); userServices.dynamicSystemCode.put(componentName, dynamicSystemCode); success = writeDynamicSystemCodeNfcid2Locked(); if (success) { service.setOrReplaceDynamicSystemCode(systemCode); newServices = new ArrayList(userServices.services.values()); } else { Log.e(TAG, "Failed to persist System Code."); // Undo registration if (oldDynamicSystemCode == null) { userServices.dynamicSystemCode.remove(componentName); } else { userServices.dynamicSystemCode.put(componentName, oldDynamicSystemCode); } } } if (success) { // Make callback without the lock held mCallback.onNfcFServicesUpdated(userId, newServices); } return success; } public String getSystemCodeForService(int userId, int uid, ComponentName componentName) { if (DBG) Log.d(TAG, "getSystemCodeForService"); NfcFServiceInfo service = getService(userId, componentName); if (service != null) { if (service.getUid() != uid) { Log.e(TAG, "UID mismatch"); return null; } return service.getSystemCode(); } else { Log.e(TAG, "Could not find service " + componentName); return null; } } public boolean removeSystemCodeForService(int userId, int uid, ComponentName componentName) { if (DBG) Log.d(TAG, "removeSystemCodeForService"); return registerSystemCodeForService(userId, uid, componentName, "NULL"); } public boolean setNfcid2ForService(int userId, int uid, ComponentName componentName, String nfcid2) { if (DBG) Log.d(TAG, "setNfcid2ForService"); ArrayList newServices = null; boolean success; synchronized (mLock) { if (mActivated) { Log.d(TAG, "failed to set NFCID2 during activation"); return false; } UserServices userServices = findOrCreateUserLocked(userId); // Check if we can find this service NfcFServiceInfo service = getService(userId, componentName); if (service == null) { Log.e(TAG, "Service " + componentName + " does not exist."); return false; } if (service.getUid() != uid) { // This is probably a good indication something is wrong here. // Either newer service installed with different uid (but then // we should have known about it), or somebody calling us from // a different uid. Log.e(TAG, "UID mismatch."); return false; } if (!NfcFCardEmulation.isValidNfcid2(nfcid2)) { Log.e(TAG, "NFCID2 " + nfcid2 + " is not a valid NFCID2"); return false; } // Apply dynamic NFCID2 mappings nfcid2 = nfcid2.toUpperCase(); DynamicNfcid2 oldDynamicNfcid2 = userServices.dynamicNfcid2.get(componentName); DynamicNfcid2 dynamicNfcid2 = new DynamicNfcid2(uid, nfcid2); userServices.dynamicNfcid2.put(componentName, dynamicNfcid2); success = writeDynamicSystemCodeNfcid2Locked(); if (success) { service.setOrReplaceDynamicNfcid2(nfcid2); newServices = new ArrayList(userServices.services.values()); } else { Log.e(TAG, "Failed to persist NFCID2."); // Undo registration if (oldDynamicNfcid2 == null) { userServices.dynamicNfcid2.remove(componentName); } else { userServices.dynamicNfcid2.put(componentName, oldDynamicNfcid2); } } } if (success) { // Make callback without the lock held mCallback.onNfcFServicesUpdated(userId, newServices); } return success; } public String getNfcid2ForService(int userId, int uid, ComponentName componentName) { if (DBG) Log.d(TAG, "getNfcid2ForService"); NfcFServiceInfo service = getService(userId, componentName); if (service != null) { if (service.getUid() != uid) { Log.e(TAG, "UID mismatch"); return null; } return service.getNfcid2(); } else { Log.e(TAG, "Could not find service " + componentName); return null; } } public void onHostEmulationActivated() { if (DBG) Log.d(TAG, "onHostEmulationActivated"); synchronized (mLock) { mActivated = true; } } public void onHostEmulationDeactivated() { if (DBG) Log.d(TAG, "onHostEmulationDeactivated"); synchronized (mLock) { mActivated = false; } } public void onNfcDisabled() { synchronized (mLock) { mActivated = false; } } private String generateRandomNfcid2() { long min = 0L; long max = 0xFFFFFFFFFFFFL; long randomNfcid2 = (long)Math.floor(Math.random() * (max-min+1)) + min; return String.format("02FE%02X%02X%02X%02X%02X%02X", (randomNfcid2 >>> 8 * 5) & 0xFF, (randomNfcid2 >>> 8 * 4) & 0xFF, (randomNfcid2 >>> 8 * 3) & 0xFF, (randomNfcid2 >>> 8 * 2) & 0xFF, (randomNfcid2 >>> 8 * 1) & 0xFF, (randomNfcid2 >>> 8 * 0) & 0xFF); } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("Registered HCE services for current user: "); synchronized (mLock) { UserServices userServices = findOrCreateUserLocked(ActivityManager.getCurrentUser()); for (NfcFServiceInfo service : userServices.services.values()) { service.dump(fd, pw, args); pw.println(""); } pw.println(""); } } }