/*
 * 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 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.PackageManager.NameNotFoundException;
import android.content.pm.PackageManager.ResolveInfoFlags;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.nfc.cardemulation.HostNfcFService;
import android.nfc.cardemulation.NfcFCardEmulation;
import android.nfc.cardemulation.NfcFServiceInfo;
import android.os.ParcelFileDescriptor;
import android.os.UserHandle;
import android.os.UserManager;
import android.sysprop.NfcProperties;
import android.util.AtomicFile;
import android.util.Log;
import android.util.SparseArray;
import android.util.Xml;
import android.util.proto.ProtoOutputStream;

import androidx.annotation.VisibleForTesting;

import com.android.internal.annotations.GuardedBy;
import com.android.nfc.Utils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

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.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import androidx.annotation.VisibleForTesting;

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 = NfcProperties.debug_enabled().orElse(true);
    private static final boolean VDBG = false; // turn on for local testing.

    final Context mContext;
    final AtomicReference<BroadcastReceiver> mReceiver;

    final Object mLock = new Object();
    // All variables below synchronized on mLock

    // mUserHandles holds the UserHandles of all the profiles that belong to current user
    @GuardedBy("mLock")
    List<UserHandle> mUserHandles;

    // mUserServices holds the card emulation services that are running for each user
    final SparseArray<UserServices> mUserServices = new SparseArray<UserServices>();
    final Callback mCallback;
    final AtomicFile mDynamicSystemCodeNfcid2File;
    boolean mActivated = false;
    boolean mUserSwitched = false;

    public interface Callback {
        void onNfcFServicesUpdated(int userId, final List<NfcFServiceInfo> 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;
        }
    };

    @VisibleForTesting
    static class UserServices {
        /**
         * All services that have registered
         */
        final HashMap<ComponentName, NfcFServiceInfo> services =
                new HashMap<>(); // Re-built at run-time
        final HashMap<ComponentName, DynamicSystemCode> dynamicSystemCode =
                new HashMap<>(); // In memory cache of dynamic System Code store
        final HashMap<ComponentName, DynamicNfcid2> dynamicNfcid2 =
                new HashMap<>(); // 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;
    }

    private int getProfileParentId(Context context, int userId) {
        UserManager um = context.getSystemService(UserManager.class);
        UserHandle uh = um.getProfileParent(UserHandle.of(userId));
        return uh == null ? userId : uh.getIdentifier();
    }

    private int getProfileParentId(int userId) {
        return getProfileParentId(mContext.createContextAsUser(
                UserHandle.of(userId), /*flags=*/0), userId);
    }

    public RegisteredNfcFServicesCache(Context context, Callback callback) {
        this(context, callback, null);
    }

    @VisibleForTesting
    RegisteredNfcFServicesCache(Context context, Callback callback, AtomicFile atomicFile) {
        mContext = context;
        mCallback = callback;

        refreshUserProfilesLocked();

        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 (VDBG) Log.d(TAG, "Intent action: " + action);
                if (uid == -1) return;
                int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
                int currentUser = ActivityManager.getCurrentUser();
                if (currentUser != getProfileParentId(context, userId)) {
                    // Cache will automatically be updated on user switch
                    if (VDBG) Log.d(TAG, "Ignoring package change intent from non-current user");
                    return;
                }
                // If app not removed, check if the app has any valid CE services.
                if (!Intent.ACTION_PACKAGE_REMOVED.equals(action) &&
                        !Utils.hasCeServicesWithValidPermissions(mContext, intent, userId)) {
                    if (VDBG) Log.d(TAG, "Ignoring package change intent from non-CE app");
                    return;
                }
                boolean replaced = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) &&
                        (Intent.ACTION_PACKAGE_ADDED.equals(action) ||
                                Intent.ACTION_PACKAGE_REMOVED.equals(action));
                if (!replaced) {
                    invalidateCache(UserHandle.getUserHandleForUid(uid).getIdentifier());
                } else {
                    if (DBG) Log.d(TAG,
                            "Ignoring package intent due to package being replaced.");
                }
            }
        };
        mReceiver = new AtomicReference<BroadcastReceiver>(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.registerReceiverForAllUsers(mReceiver.get(), 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.registerReceiverForAllUsers(mReceiver.get(), sdFilter, null, null);

        File dataDir = mContext.getFilesDir();
        if (atomicFile == null) {
            mDynamicSystemCodeNfcid2File =
                new AtomicFile(new File(dataDir, "dynamic_systemcode_nfcid2.xml"));
        } else {
            mDynamicSystemCodeNfcid2File = atomicFile;
        }
    }

    void initialize() {
        synchronized (mLock) {
            readDynamicSystemCodeNfcid2Locked();
            for (UserHandle uh : mUserHandles) {
                invalidateCache(uh.getIdentifier());
            }
        }
    }

    void dump(ArrayList<NfcFServiceInfo> services) {
        for (NfcFServiceInfo service : services) {
            Log.d(TAG, service.toString());
        }
    }

    boolean containsServiceLocked(ArrayList<NfcFServiceInfo> 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<NfcFServiceInfo> getServices(int userId) {
        final ArrayList<NfcFServiceInfo> services = new ArrayList<NfcFServiceInfo>();
        synchronized (mLock) {
            UserServices userServices = findOrCreateUserLocked(userId);
            services.addAll(userServices.services.values());
        }
        return services;
    }

    ArrayList<NfcFServiceInfo> getInstalledServices(int userId) {
        if (DBG) Log.d(TAG, "getInstalledServices");
        PackageManager pm;
        try {
            pm = mContext.createPackageContextAsUser("android", 0,
                    UserHandle.of(userId)).getPackageManager();
        } catch (NameNotFoundException e) {
            Log.e(TAG, "Could not create user package context");
            return null;
        }

        ArrayList<NfcFServiceInfo> validServices = new ArrayList<NfcFServiceInfo>();

        List<ResolveInfo> resolvedServices = pm.queryIntentServicesAsUser(
                new Intent(HostNfcFService.SERVICE_INTERFACE),
                ResolveInfoFlags.of(PackageManager.GET_META_DATA), UserHandle.of(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<NfcFServiceInfo> validServices = getInstalledServices(userId);
        if (validServices == null) {
            return;
        }
        ArrayList<NfcFServiceInfo> newServices = null;
        ArrayList<NfcFServiceInfo> toBeAdded = new ArrayList<>();
        ArrayList<NfcFServiceInfo> toBeRemoved = new ArrayList<>();
        synchronized (mLock) {
            UserServices userServices = findOrCreateUserLocked(userId);

            // Check update
            ArrayList<NfcFServiceInfo> cachedServices =
                    new ArrayList<NfcFServiceInfo>(userServices.services.values());
            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 (mUserSwitched) {
                Log.d(TAG, "User switched, rebuild internal cache");
                mUserSwitched = false;
            } else 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);
            }
            for (NfcFServiceInfo service : toBeRemoved) {
                userServices.services.remove(service.getComponent());
            }
            // Apply dynamic System Code mappings
            ArrayList<ComponentName> toBeRemovedDynamicSystemCode =
                    new ArrayList<ComponentName>();
            for (Map.Entry<ComponentName, DynamicSystemCode> 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.setDynamicSystemCode(dynamicSystemCode.systemCode);
                }
            }
            // Apply dynamic NFCID2 mappings
            ArrayList<ComponentName> toBeRemovedDynamicNfcid2 =
                    new ArrayList<ComponentName>();
            for (Map.Entry<ComponentName, DynamicNfcid2> 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.setDynamicNfcid2(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<ComponentName, NfcFServiceInfo> entry :
                userServices.services.entrySet()) {
                NfcFServiceInfo service = entry.getValue();
                if (service.getNfcid2().equalsIgnoreCase("RANDOM")) {
                    String randomNfcid2 = generateRandomNfcid2();
                    service.setDynamicNfcid2(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<NfcFServiceInfo>(userServices.services.values());
        }
        mCallback.onNfcFServicesUpdated(userId, Collections.unmodifiableList(newServices));
        if (VDBG) {
            Log.i(TAG, "Services => ");
            dump(newServices);
        } else {
            // dump only new services added or removed
            Log.i(TAG, "New Services => ");
            dump(toBeAdded);
            Log.i(TAG, "Removed Services => ");
            dump(toBeRemoved);
        }
    }

    @VisibleForTesting
    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");
                            if (compString == null || uidString == null) {
                                Log.e(TAG, "Invalid service attributes");
                            } else {
                                try {
                                    componentName = ComponentName.unflattenFromString(compString);
                                    currentUid = Integer.parseInt(uidString);
                                    systemCode = parser.getAttributeValue(null, "system-code");
                                    description = parser.getAttributeValue(null, "description");
                                    nfcid2 = parser.getAttributeValue(null, "nfcid2");
                                } 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.
                                        getUserHandleForUid(currentUid).getIdentifier();
                                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) {
                }
            }
        }
    }

    @VisibleForTesting
    boolean writeDynamicSystemCodeNfcid2Locked() {
        if (DBG) Log.d(TAG, "writeDynamicSystemCodeNfcid2Locked");
        FileOutputStream fos = null;
        try {
            fos = mDynamicSystemCodeNfcid2File.startWrite();
            XmlSerializer out = Xml.newSerializer();
            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<ComponentName, DynamicSystemCode> 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<ComponentName, DynamicNfcid2> 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<NfcFServiceInfo> 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.setDynamicSystemCode(systemCode);
                newServices = new ArrayList<NfcFServiceInfo>(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<NfcFServiceInfo> 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.setDynamicNfcid2(nfcid2);
                newServices = new ArrayList<NfcFServiceInfo>(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;
        }
    }

    public void onUserSwitched() {
        synchronized (mLock) {
            mUserSwitched = true;
            refreshUserProfilesLocked();
        }
    }

    public void onManagedProfileChanged() {
        synchronized (mLock) {
            refreshUserProfilesLocked();
        }
    }

    private void refreshUserProfilesLocked() {
        UserManager um = mContext.createContextAsUser(
                UserHandle.of(ActivityManager.getCurrentUser()), /*flags=*/0)
                .getSystemService(UserManager.class);
        mUserHandles = um.getEnabledProfiles();
        List<UserHandle> removeUserHandles = new ArrayList<UserHandle>();

        for (UserHandle uh : mUserHandles) {
            if (um.isQuietModeEnabled(uh)) {
                removeUserHandles.add(uh);
            }
        }
        mUserHandles.removeAll(removeUserHandles);
    }

    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-F services for current user: ");
        ParcelFileDescriptor pFd;
        try {
            pFd = ParcelFileDescriptor.dup(fd);
            synchronized (mLock) {
                for (UserHandle uh : mUserHandles) {
                    UserManager um = mContext.createContextAsUser(
                            uh, /*flags=*/0).getSystemService(UserManager.class);
                    pw.println("User " + Utils.maskSubstring(um.getUserName(), 3));
                    UserServices userServices = findOrCreateUserLocked(uh.getIdentifier());
                    for (NfcFServiceInfo service : userServices.services.values()) {
                        service.dump(pFd, pw, args);
                        pw.println("");
                    }
                    pw.println("");
                }
            }
            pFd.close();
        } catch (IOException e) {
            pw.println("Failed to dump HCE-F services: " + e);
        }
    }

    /**
     * Dump debugging information as a RegisteredNfcFServicesCacheProto
     *
     * Note:
     * See proto definition in frameworks/base/core/proto/android/nfc/card_emulation.proto
     * When writing a nested message, must call {@link ProtoOutputStream#start(long)} before and
     * {@link ProtoOutputStream#end(long)} after.
     * Never reuse a proto field number. When removing a field, mark it as reserved.
     */
    void dumpDebug(ProtoOutputStream proto) {
        synchronized (mLock) {
            UserServices userServices = findOrCreateUserLocked(ActivityManager.getCurrentUser());
            for (NfcFServiceInfo service : userServices.services.values()) {
                long token = proto.start(RegisteredNfcFServicesCacheProto.NFC_FSERVICE_INFO);
                service.dumpDebug(proto);
                proto.end(token);
            }
        }
    }

    @VisibleForTesting
    public boolean isActivated() {
        return mActivated;
    }

}
