/*
 * Copyright (C) 2024 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 android.nfc;

import static android.nfc.cardemulation.CardEmulation.PROTOCOL_AND_TECHNOLOGY_ROUTE_DH;
import static android.nfc.cardemulation.CardEmulation.PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE;
import static android.nfc.cardemulation.CardEmulation.PROTOCOL_AND_TECHNOLOGY_ROUTE_NDEF_NFCEE;
import static android.nfc.cardemulation.CardEmulation.PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC;
import static android.nfc.cardemulation.CardEmulation.routeIntToString;

import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.DurationMillisLong;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.nfc.cardemulation.ApduServiceInfo;
import android.nfc.cardemulation.CardEmulation;
import android.nfc.cardemulation.CardEmulation.ProtocolAndTechnologyRoute;
import android.os.Binder;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.se.omapi.Reader;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * Used for OEM extension APIs.
 * This class holds all the APIs and callbacks defined for OEMs/vendors to extend the NFC stack
 * for their proprietary features.
 *
 * @hide
 */
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@SystemApi
public final class NfcOemExtension {
    private static final String TAG = "NfcOemExtension";
    private static final int OEM_EXTENSION_RESPONSE_THRESHOLD_MS = 2000;
    private static final int TYPE_TECHNOLOGY = 0;
    private static final int TYPE_PROTOCOL = 1;
    private static final int TYPE_AID = 2;
    private static final int TYPE_SYSTEMCODE = 3;

    private final NfcAdapter mAdapter;
    private final NfcOemExtensionCallback mOemNfcExtensionCallback;
    private boolean mIsRegistered = false;
    private final Map<Callback, Executor> mCallbackMap = new HashMap<>();
    private final Context mContext;
    private final Object mLock = new Object();
    private boolean mCardEmulationActivated = false;
    private boolean mRfFieldActivated = false;
    private boolean mRfDiscoveryStarted = false;
    private boolean mEeListenActivated = false;

    /**
     * Broadcast Action: Sent on NFC stack initialization when NFC OEM extensions are enabled.
     * <p> OEM extension modules should use this intent to start their extension service </p>
     * @hide
     */
    public static final String ACTION_OEM_EXTENSION_INIT = "android.nfc.action.OEM_EXTENSION_INIT";

    /**
     * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
     * Enables the controller in default mode when NFC is disabled (existing API behavior).
     * works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
     * @hide
     */
    @SystemApi
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public static final int ENABLE_DEFAULT = NfcAdapter.CONTROLLER_ALWAYS_ON_MODE_DEFAULT;

    /**
     * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
     * Enables the controller in transparent mode when NFC is disabled.
     * @hide
     */
    @SystemApi
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public static final int ENABLE_TRANSPARENT = 2;

    /**
     * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
     * Enables the controller and initializes and enables the EE subsystem when NFC is disabled.
     * @hide
     */
    @SystemApi
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public static final int ENABLE_EE = 3;

    /**
     * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
     * Disable the Controller Always On Mode.
     * works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
     * @hide
     */
    @SystemApi
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public static final int DISABLE = NfcAdapter.CONTROLLER_ALWAYS_ON_DISABLE;

    /**
     * Possible controller modes for {@link #setControllerAlwaysOnMode(int)}.
     *
     * @hide
     */
    @IntDef(prefix = { "" }, value = {
        ENABLE_DEFAULT,
        ENABLE_TRANSPARENT,
        ENABLE_EE,
        DISABLE,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ControllerMode{}

    /**
     * Technology Type for {@link #getActiveNfceeList()}.
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public static final int NFCEE_TECH_NONE = 0;

    /**
     * Technology Type for {@link #getActiveNfceeList()}.
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public static final int NFCEE_TECH_A = 1;

    /**
     * Technology Type for {@link #getActiveNfceeList()}.
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public static final int NFCEE_TECH_B = 1 << 1;

    /**
     * Technology Type for {@link #getActiveNfceeList()}.
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public static final int NFCEE_TECH_F = 1 << 2;

    /**
     * Nfc technology flags for {@link #getActiveNfceeList()}.
     *
     * @hide
     */
    @IntDef(flag = true, value = {
        NFCEE_TECH_NONE,
        NFCEE_TECH_A,
        NFCEE_TECH_B,
        NFCEE_TECH_F,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface NfceeTechnology {}

    /**
     * Event that Host Card Emulation is activated.
     */
    public static final int HCE_ACTIVATE = 1;
    /**
     * Event that some data is transferred in Host Card Emulation.
     */
    public static final int HCE_DATA_TRANSFERRED = 2;
    /**
     * Event that Host Card Emulation is deactivated.
     */
    public static final int HCE_DEACTIVATE = 3;
    /**
     * Possible events from {@link Callback#onHceEventReceived}.
     *
     * @hide
     */
    @IntDef(value = {
            HCE_ACTIVATE,
            HCE_DATA_TRANSFERRED,
            HCE_DEACTIVATE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface HostCardEmulationAction {}

    /**
     * Status code returned when the polling state change request succeeded.
     * @see #pausePolling()
     * @see #resumePolling()
     */
    public static final int POLLING_STATE_CHANGE_SUCCEEDED = 1;
    /**
     * Status code returned when the polling state change request is already in
     * required state.
     * @see #pausePolling()
     * @see #resumePolling()
     */
    public static final int POLLING_STATE_CHANGE_ALREADY_IN_REQUESTED_STATE = 2;
    /**
     * Possible status codes for {@link #pausePolling()} and
     * {@link #resumePolling()}.
     * @hide
     */
    @IntDef(value = {
            POLLING_STATE_CHANGE_SUCCEEDED,
            POLLING_STATE_CHANGE_ALREADY_IN_REQUESTED_STATE,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface PollingStateChangeStatusCode {}

    /**
     * Status OK
     */
    public static final int STATUS_OK = 0;
    /**
     * Status unknown error
     */
    public static final int STATUS_UNKNOWN_ERROR = 1;

    /**
     * Status codes passed to OEM extension callbacks.
     *
     * @hide
     */
    @IntDef(value = {
            STATUS_OK,
            STATUS_UNKNOWN_ERROR
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface StatusCode {}

    /**
     * Routing commit succeeded.
     */
    public static final int COMMIT_ROUTING_STATUS_OK = 0;
    /**
     * Routing commit failed.
     */
    public static final int COMMIT_ROUTING_STATUS_FAILED = 3;
    /**
     * Routing commit failed due to the update is in progress.
     */
    public static final int COMMIT_ROUTING_STATUS_FAILED_UPDATE_IN_PROGRESS = 6;

    /**
     * Status codes returned when calling {@link #forceRoutingTableCommit()}
     * @hide
     */
    @IntDef(prefix = "COMMIT_ROUTING_STATUS_", value = {
            COMMIT_ROUTING_STATUS_OK,
            COMMIT_ROUTING_STATUS_FAILED,
            COMMIT_ROUTING_STATUS_FAILED_UPDATE_IN_PROGRESS,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface CommitRoutingStatusCode {}
    /**
     * Interface for Oem extensions for NFC.
     */
    public interface Callback {
        /**
         * Notify Oem to tag is connected or not
         * ex - if tag is connected  notify cover and Nfctest app if app is in testing mode
         *
         * @param connected status of the tag true if tag is connected otherwise false
         */
        void onTagConnected(boolean connected);

        /**
         * Update the Nfc Adapter State
         * @param state new state that need to be updated
         */
        void onStateUpdated(@NfcAdapter.AdapterState int state);
        /**
         * Check if NfcService apply routing method need to be skipped for
         * some feature.
         * @param isSkipped The {@link Consumer} to be completed. If apply routing can be skipped,
         *                  the {@link Consumer#accept(Object)} should be called with
         *                  {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
         */
        void onApplyRouting(@NonNull Consumer<Boolean> isSkipped);
        /**
         * Check if NfcService ndefRead method need to be skipped To skip
         * and start checking for presence of tag
         * @param isSkipped The {@link Consumer} to be completed. If Ndef read can be skipped,
         *                  the {@link Consumer#accept(Object)} should be called with
         *                  {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
         */
        void onNdefRead(@NonNull Consumer<Boolean> isSkipped);
        /**
         * Method to check if Nfc is allowed to be enabled by OEMs.
         * @param isAllowed The {@link Consumer} to be completed. If enabling NFC is allowed,
         *                  the {@link Consumer#accept(Object)} should be called with
         *                  {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
         * false if NFC cannot be enabled at this time.
         */
        void onEnableRequested(@NonNull Consumer<Boolean> isAllowed);
        /**
         * Method to check if Nfc is allowed to be disabled by OEMs.
         * @param isAllowed The {@link Consumer} to be completed. If disabling NFC is allowed,
         *                  the {@link Consumer#accept(Object)} should be called with
         *                  {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
         * false if NFC cannot be disabled at this time.
         */
        void onDisableRequested(@NonNull Consumer<Boolean> isAllowed);

        /**
         * Callback to indicate that Nfc starts to boot.
         */
        void onBootStarted();

        /**
         * Callback to indicate that Nfc starts to enable.
         */
        void onEnableStarted();

        /**
         * Callback to indicate that Nfc starts to disable.
         */
        void onDisableStarted();

        /**
         * Callback to indicate if NFC boots successfully or not.
         * @param status the status code indicating if boot finished successfully
         */
        void onBootFinished(@StatusCode int status);

        /**
         * Callback to indicate if NFC is successfully enabled.
         * @param status the status code indicating if enable finished successfully
         */
        void onEnableFinished(@StatusCode int status);

        /**
         * Callback to indicate if NFC is successfully disabled.
         * @param status the status code indicating if disable finished successfully
         */
        void onDisableFinished(@StatusCode int status);

        /**
         * Check if NfcService tag dispatch need to be skipped.
         * @param isSkipped The {@link Consumer} to be completed. If tag dispatch can be skipped,
         *                  the {@link Consumer#accept(Object)} should be called with
         *                  {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
         */
        void onTagDispatch(@NonNull Consumer<Boolean> isSkipped);

        /**
         * Notifies routing configuration is changed.
         * @param isCommitRoutingSkipped The {@link Consumer} to be
         * completed. If routing commit should be skipped,
         * the {@link Consumer#accept(Object)} should be called with
         * {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
         */
        void onRoutingChanged(@NonNull Consumer<Boolean> isCommitRoutingSkipped);

        /**
         * API to activate start stop cpu boost on hce event.
         *
         * <p>When HCE is activated, transferring data, and deactivated,
         * must call this method to activate, start and stop cpu boost respectively.
         * @param action Flag indicating actions to activate, start and stop cpu boost.
         */
        void onHceEventReceived(@HostCardEmulationAction int action);

        /**
         * API to notify when reader option has been changed using
         * {@link NfcAdapter#enableReaderOption(boolean)} by some app.
         * @param enabled Flag indicating ReaderMode enabled/disabled
         */
        void onReaderOptionChanged(boolean enabled);

        /**
        * Notifies NFC is activated in listen mode.
        * NFC Forum NCI-2.3 ch.5.2.6 specification
        *
        * <p>NFCC is ready to communicate with a Card reader
        *
        * @param isActivated true, if card emulation activated, else de-activated.
        */
        void onCardEmulationActivated(boolean isActivated);

        /**
        * Notifies the Remote NFC Endpoint RF Field is detected.
        * NFC Forum NCI-2.3 ch.5.3 specification
        *
        * @param isActive true, if RF Field is ON, else RF Field is OFF.
        */
        void onRfFieldDetected(boolean isActive);

        /**
        * Notifies the NFC RF discovery is started or in the IDLE state.
        * NFC Forum NCI-2.3 ch.5.2 specification
        *
        * @param isDiscoveryStarted true, if RF discovery started, else RF state is Idle.
        */
        void onRfDiscoveryStarted(boolean isDiscoveryStarted);

        /**
        * Notifies the NFCEE (NFC Execution Environment) Listen has been activated.
        *
        * @param isActivated true, if EE Listen is ON, else EE Listen is OFF.
        */
        void onEeListenActivated(boolean isActivated);

        /**
        * Notifies that some NFCEE (NFC Execution Environment) has been updated.
        *
        * <p> This indicates that some applet has been installed/updated/removed in
        * one of the NFCEE's.
        * </p>
        */
        void onEeUpdated();

        /**
         * Gets the intent to find the OEM package in the OEM App market. If the consumer returns
         * {@code null} or a timeout occurs, the intent from the first available package will be
         * used instead.
         *
         * @param packages the OEM packages name stored in the tag
         * @param intentConsumer The {@link Consumer} to be completed.
         *                       The {@link Consumer#accept(Object)} should be called with
         *                       the Intent required.
         *
         */
        void onGetOemAppSearchIntent(@NonNull List<String> packages,
                                     @NonNull Consumer<Intent> intentConsumer);

        /**
         * Checks if the NDEF message contains any specific OEM package executable content
         *
         * @param tag        the {@link android.nfc.Tag Tag}
         * @param message NDEF Message to read from tag
         * @param hasOemExecutableContent The {@link Consumer} to be completed. If there is
         *                                OEM package executable content, the
         *                                {@link Consumer#accept(Object)} should be called with
         *                                {@link Boolean#TRUE}, otherwise call with
         *                                {@link Boolean#FALSE}.
         */
        void onNdefMessage(@NonNull Tag tag, @NonNull NdefMessage message,
                           @NonNull Consumer<Boolean> hasOemExecutableContent);

        /**
         * Callback to indicate the app chooser activity should be launched for handling CE
         * transaction. This is invoked for example when there are more than 1 app installed that
         * can handle the HCE transaction. OEMs can launch the Activity based
         * on their requirement.
         *
         * @param selectedAid the selected AID from APDU
         * @param services {@link ApduServiceInfo} of the service triggering the activity
         * @param failedComponent the component failed to be resolved
         * @param category the category of the service
         */
        void onLaunchHceAppChooserActivity(@NonNull String selectedAid,
                                           @NonNull List<ApduServiceInfo> services,
                                           @NonNull ComponentName failedComponent,
                                           @NonNull String category);

        /**
         * Callback to indicate tap again dialog should be launched for handling HCE transaction.
         * This is invoked for example when a CE service needs the device to unlocked before
         * handling the transaction. OEMs can launch the Activity based on their requirement.
         *
         * @param service {@link ApduServiceInfo} of the service triggering the dialog
         * @param category the category of the service
         */
        void onLaunchHceTapAgainDialog(@NonNull ApduServiceInfo service, @NonNull String category);

        /**
         * Callback to indicate that routing table is full and the OEM can optionally launch a
         * dialog to request the user to remove some Card Emulation apps from the device to free
         * routing table space.
         */
        void onRoutingTableFull();

        /**
         * Callback when OEM specified log event are notified.
         * @param item the log items that contains log information of NFC event.
         */
        void onLogEventNotified(@NonNull OemLogItems item);

        /**
         * Callback to to extract OEM defined packages from given NDEF message when
         * a NFC tag is detected. These are used to handle NFC tags encoded with a
         * proprietary format for storing app name (Android native app format).
         *
         * @param message NDEF message containing OEM package names
         * @param packageConsumer The {@link Consumer} to be completed.
         *                        The {@link Consumer#accept(Object)} should be called with
         *                        the list of package names.
         */
        void onExtractOemPackages(@NonNull NdefMessage message,
                @NonNull Consumer<List<String>> packageConsumer);
    }


    /**
     * Constructor to be used only by {@link NfcAdapter}.
     */
    NfcOemExtension(@NonNull Context context, @NonNull NfcAdapter adapter) {
        mContext = context;
        mAdapter = adapter;
        mOemNfcExtensionCallback = new NfcOemExtensionCallback();
    }

    /** @hide */
    @VisibleForTesting
    public NfcOemExtensionCallback getOemNfcExtensionCallback() {
        return mOemNfcExtensionCallback;
    }

    /**
     * Get an instance of {@link T4tNdefNfcee} object for performing T4T (Type-4 Tag)
     * NDEF (NFC Data Exchange Format) NFCEE (NFC Execution Environment) operations.
     * This can be used to write NDEF data to emulate a T4T tag in an NFCEE
     * (NFC Execution Environment - eSE, SIM, etc). Refer to the NFC forum specification
     * "NFCForum-TS-NCI-2.3 section 10.4" and "NFCForum-TS-T4T-1.1 section 4.2" for more details.
     *
     * This is a singleton object which shall be used by OEM extension module to do NDEF-NFCEE
     * read/write operations.
     *
     * <p>Returns {@link T4tNdefNfcee}
     * <p>Does not cause any RF activity and does not block.
     * @return NFC Data Exchange Format (NDEF) NFC Execution Environment (NFCEE) object
     * @hide
     */
    @SystemApi
    @NonNull
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public T4tNdefNfcee getT4tNdefNfcee() {
        return T4tNdefNfcee.getInstance();
    }

    /**
     * Register an {@link Callback} to listen for NFC oem extension callbacks
     * Multiple clients can register and callbacks will be invoked asynchronously.
     *
     * <p>The provided callback will be invoked by the given {@link Executor}.
     * As part of {@link #registerCallback(Executor, Callback)} the
     * {@link Callback} will be invoked with current NFC state
     * before the {@link #registerCallback(Executor, Callback)} function completes.
     *
     * @param executor an {@link Executor} to execute given callback
     * @param callback oem implementation of {@link Callback}
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
    public void registerCallback(@NonNull @CallbackExecutor Executor executor,
            @NonNull Callback callback) {
        synchronized (mLock) {
            if (executor == null || callback == null) {
                Log.e(TAG, "Executor and Callback must not be null!");
                throw new IllegalArgumentException();
            }

            if (mCallbackMap.containsKey(callback)) {
                Log.e(TAG, "Callback already registered. Unregister existing callback before"
                        + "registering");
                throw new IllegalArgumentException();
            }
            mCallbackMap.put(callback, executor);
            if (!mIsRegistered) {
                NfcAdapter.callService(() -> {
                    NfcAdapter.sService.registerOemExtensionCallback(mOemNfcExtensionCallback);
                    mIsRegistered = true;
                });
            } else {
                updateNfCState(callback, executor);
            }
        }
    }

    private void updateNfCState(Callback callback, Executor executor) {
        if (callback != null) {
            Log.i(TAG, "updateNfCState");
            executor.execute(() -> {
                callback.onCardEmulationActivated(mCardEmulationActivated);
                callback.onRfFieldDetected(mRfFieldActivated);
                callback.onRfDiscoveryStarted(mRfDiscoveryStarted);
                callback.onEeListenActivated(mEeListenActivated);
            });
        }
    }

    /**
     * Unregister the specified {@link Callback}
     *
     * <p>The same {@link Callback} object used when calling
     * {@link #registerCallback(Executor, Callback)} must be used.
     *
     * <p>Callbacks are automatically unregistered when an application process goes away
     *
     * @param callback oem implementation of {@link Callback}
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
    public void unregisterCallback(@NonNull Callback callback) {
        synchronized (mLock) {
            if (!mCallbackMap.containsKey(callback) || !mIsRegistered) {
                Log.e(TAG, "Callback not registered");
                throw new IllegalArgumentException();
            }
            if (mCallbackMap.size() == 1) {
                NfcAdapter.callService(() -> {
                    NfcAdapter.sService.unregisterOemExtensionCallback(mOemNfcExtensionCallback);
                    mIsRegistered = false;
                    mCallbackMap.remove(callback);
                });
            } else {
                mCallbackMap.remove(callback);
            }
        }
    }

    /**
     * Clear NfcService preference, interface method to clear NFC preference values on OEM specific
     * events. For ex: on soft reset, Nfc default values needs to be overridden by OEM defaults.
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
    public void clearPreference() {
        NfcAdapter.callService(() -> NfcAdapter.sService.clearPreference());
    }

    /**
     * Get the screen state from system and set it to current screen state.
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
    public void synchronizeScreenState() {
        NfcAdapter.callService(() -> NfcAdapter.sService.setScreenState());
    }

    /**
     * Check if the firmware needs updating.
     *
     * <p>If an update is needed, a firmware will be triggered when NFC is disabled.
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
    public void maybeTriggerFirmwareUpdate() {
        NfcAdapter.callService(() -> NfcAdapter.sService.checkFirmware());
    }

    /**
     * Get the Active NFCEE (NFC Execution Environment) List
     *
     * @return Map< String, @NfceeTechnology Integer >
     *         A HashMap where keys are activated secure elements and
     *         the values are bitmap of technologies supported by each secure element:
     *          NFCEE_TECH_A == 0x1
     *          NFCEE_TECH_B == 0x2
     *          NFCEE_TECH_F == 0x4
     *         and keys can contain "eSE" and "SIM" with a number,
     *         in case of failure an empty map is returned.
     *         @see Reader#getName() for the list of possible NFCEE names.
     */
    @NonNull
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public Map<String, Integer> getActiveNfceeList() {
        return NfcAdapter.callServiceReturn(() ->
            NfcAdapter.sService.fetchActiveNfceeList(), new HashMap<String, Integer>());
    }

    /**
     * Sets NFC controller always on feature.
     * <p>This API is for the NFCC internal state management. It allows to discriminate
     * the controller function from the NFC function by keeping the NFC controller on without
     * any NFC RF enabled if necessary.
     * <p>This call is asynchronous, register listener {@link NfcAdapter.ControllerAlwaysOnListener}
     * by {@link NfcAdapter#registerControllerAlwaysOnListener} to find out when the operation is
     * complete.
     * <p> Note: This adds more always on modes on top of existing
     * {@link NfcAdapter#setControllerAlwaysOn(boolean)} API which can be used to set the NFCC in
     * only {@link #ENABLE_DEFAULT} and {@link #DISABLE} modes.
     * @param mode one of {@link ControllerMode} modes
     * @throws UnsupportedOperationException if
     *   <li> if FEATURE_NFC, FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
     *   FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE
     *   are unavailable </li>
     *   <li> if the feature is unavailable @see NfcAdapter#isNfcControllerAlwaysOnSupported() </li>
     * @hide
     * @see NfcAdapter#setControllerAlwaysOn(boolean)
     */
    @SystemApi
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON)
    public void setControllerAlwaysOnMode(@ControllerMode int mode) {
        if (!NfcAdapter.sHasNfcFeature && !NfcAdapter.sHasCeFeature) {
            throw new UnsupportedOperationException();
        }
        NfcAdapter.callService(() -> NfcAdapter.sService.setControllerAlwaysOn(mode));
    }

    /**
     * Triggers NFC initialization. If OEM extension is registered
     * (indicated via `enable_oem_extension` NFC overlay), the NFC stack initialization at bootup
     * is delayed until the OEM extension app triggers the initialization via this call.
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
    public void triggerInitialization() {
        NfcAdapter.callService(() -> NfcAdapter.sService.triggerInitialization());
    }

    /**
     * Gets the last user toggle status.
     * @return true if NFC is set to ON, false otherwise
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
    public boolean hasUserEnabledNfc() {
        return NfcAdapter.callServiceReturn(() -> NfcAdapter.sService.getSettingStatus(), false);
    }

    /**
     * Checks if the tag is present or not.
     * @return true if the tag is present, false otherwise
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
    public boolean isTagPresent() {
        return NfcAdapter.callServiceReturn(() -> NfcAdapter.sService.isTagPresent(), false);
    }

    /**
     * Pauses NFC tag reader mode polling for a {@code timeoutInMs} millisecond.
     * In case of {@code timeoutInMs} is zero, polling will be stopped indefinitely.
     * Use {@link #resumePolling()} to resume the polling.
     * Use {@link #getMaxPausePollingTimeoutMs()} to check the max timeout value.
     * @param timeoutInMs the pause polling duration in millisecond.
     * @return status of the operation
     * @throws IllegalArgumentException if timeoutInMs value is invalid
     *         (timeoutinMs > max or timeoutInMs < 0).
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
    public @PollingStateChangeStatusCode int pausePolling(@DurationMillisLong long timeoutInMs) {
        return NfcAdapter.callServiceReturn(() ->
                NfcAdapter.sService.pausePolling(timeoutInMs),
                POLLING_STATE_CHANGE_ALREADY_IN_REQUESTED_STATE);
    }

    /**
     * Resumes default NFC tag reader mode polling for the current device state if polling is
     * paused. Calling this while already in polling is a no-op.
     * @return status of the operation
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
    public @PollingStateChangeStatusCode int resumePolling() {
        return NfcAdapter.callServiceReturn(() ->
                NfcAdapter.sService.resumePolling(),
                POLLING_STATE_CHANGE_ALREADY_IN_REQUESTED_STATE);
    }

    /**
     * Gets the max pause polling timeout value in millisecond.
     * @return long integer representing the max timeout
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
    @DurationMillisLong
    public long getMaxPausePollingTimeoutMills() {
        return NfcAdapter.callServiceReturn(() ->
                NfcAdapter.sService.getMaxPausePollingTimeoutMs(), 0L);
    }

    /**
     * Set whether to enable auto routing change or not (enabled by default).
     * If disabled, routing targets are limited to a single off-host destination.
     *
     * @param state status of auto routing change, true if enable, otherwise false
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
    public void setAutoChangeEnabled(boolean state) {
        NfcAdapter.callService(() ->
                NfcAdapter.sCardEmulationService.setAutoChangeStatus(state));
    }

    /**
     * Check if auto routing change is enabled or not.
     *
     * @return true if enabled, otherwise false
     */
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
    public boolean isAutoChangeEnabled() {
        return NfcAdapter.callServiceReturn(() ->
                NfcAdapter.sCardEmulationService.isAutoChangeEnabled(), false);
    }

    /**
     * Get current routing status
     *
     * @return {@link RoutingStatus} indicating the default route, default ISO-DEP
     * route and default off-host route.
     */
    @NonNull
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
    public RoutingStatus getRoutingStatus() {
        List<String> status = NfcAdapter.callServiceReturn(() ->
                NfcAdapter.sCardEmulationService.getRoutingStatus(), new ArrayList<>());
        return new RoutingStatus(routeStringToInt(status.get(0)),
                routeStringToInt(status.get(1)),
                routeStringToInt(status.get(2)));
    }

    /**
     * Overwrites NFC controller routing table, which includes Protocol Route, Technology Route,
     * and Empty AID Route.
     *
     * The parameter set to
     * {@link ProtocolAndTechnologyRoute#PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET}
     * can be used to keep current values for that entry. At least one route should be overridden
     * when calling this API, otherwise throw {@link IllegalArgumentException}.
     *
     * @param protocol ISO-DEP route destination, where the possible inputs are defined in
     *                 {@link ProtocolAndTechnologyRoute}.
     * @param technology Tech-A, Tech-B and Tech-F route destination, where the possible inputs
     *                   are defined in
     *                   {@link ProtocolAndTechnologyRoute}
     * @param emptyAid Zero-length AID route destination, where the possible inputs are defined in
     *                 {@link ProtocolAndTechnologyRoute}
     * @param systemCode System Code route destination, where the possible inputs are defined in
     *                   {@link ProtocolAndTechnologyRoute}
     */
    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public void overwriteRoutingTable(
            @CardEmulation.ProtocolAndTechnologyRoute int protocol,
            @CardEmulation.ProtocolAndTechnologyRoute int technology,
            @CardEmulation.ProtocolAndTechnologyRoute int emptyAid,
            @CardEmulation.ProtocolAndTechnologyRoute int systemCode) {

        String protocolRoute = routeIntToString(protocol);
        String technologyRoute = routeIntToString(technology);
        String emptyAidRoute = routeIntToString(emptyAid);
        String systemCodeRoute = routeIntToString(systemCode);

        NfcAdapter.callService(() ->
                NfcAdapter.sCardEmulationService.overwriteRoutingTable(
                        mContext.getUser().getIdentifier(),
                        emptyAidRoute,
                        protocolRoute,
                        technologyRoute,
                        systemCodeRoute
                ));
    }

    /**
     * Gets current routing table entries.
     * @return List of {@link NfcRoutingTableEntry} representing current routing table
     */
    @NonNull
    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    public List<NfcRoutingTableEntry> getRoutingTable() {
        List<Entry> entryList = NfcAdapter.callServiceReturn(() ->
                NfcAdapter.sService.getRoutingTableEntryList(), null);
        List<NfcRoutingTableEntry> result = new ArrayList<>();
        for (Entry entry : entryList) {
            switch (entry.getType()) {
                case TYPE_TECHNOLOGY -> result.add(
                        new RoutingTableTechnologyEntry(entry.getNfceeId(),
                                RoutingTableTechnologyEntry.techStringToInt(entry.getEntry()),
                                routeStringToInt(entry.getRoutingType()))
                );
                case TYPE_PROTOCOL -> result.add(
                        new RoutingTableProtocolEntry(entry.getNfceeId(),
                                RoutingTableProtocolEntry.protocolStringToInt(entry.getEntry()),
                                routeStringToInt(entry.getRoutingType()))
                );
                case TYPE_AID -> result.add(
                        new RoutingTableAidEntry(entry.getNfceeId(), entry.getEntry(),
                                routeStringToInt(entry.getRoutingType()))
                );
                case TYPE_SYSTEMCODE -> result.add(
                        new RoutingTableSystemCodeEntry(entry.getNfceeId(),
                                entry.getEntry().getBytes(StandardCharsets.UTF_8),
                                routeStringToInt(entry.getRoutingType()))
                );
            }
        }
        return result;
    }

    /**
     * API to force a routing table commit.
     * @return a {@link StatusCode} to indicate if commit routing succeeded or not
     */
    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
    @CommitRoutingStatusCode
    public int forceRoutingTableCommit() {
        return NfcAdapter.callServiceReturn(
                () -> NfcAdapter.sService.commitRouting(), COMMIT_ROUTING_STATUS_FAILED);
    }

    /** @hide */
    public final class NfcOemExtensionCallback extends INfcOemExtensionCallback.Stub {

        @Override
        public void onTagConnected(boolean connected) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(connected, cb::onTagConnected, ex));
        }

        @Override
        public void onCardEmulationActivated(boolean isActivated) throws RemoteException {
            mCardEmulationActivated = isActivated;
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(isActivated, cb::onCardEmulationActivated, ex));
        }

        @Override
        public void onRfFieldDetected(boolean isActive) throws RemoteException {
            mRfFieldActivated = isActive;
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(isActive, cb::onRfFieldDetected, ex));
        }

        @Override
        public void onRfDiscoveryStarted(boolean isDiscoveryStarted) throws RemoteException {
            mRfDiscoveryStarted = isDiscoveryStarted;
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(isDiscoveryStarted, cb::onRfDiscoveryStarted, ex));
        }

        @Override
        public void onEeListenActivated(boolean isActivated) throws RemoteException {
            mEeListenActivated = isActivated;
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(isActivated, cb::onEeListenActivated, ex));
        }

        @Override
        public void onEeUpdated() throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(null, (Object input) -> cb.onEeUpdated(), ex));
        }

        @Override
        public void onStateUpdated(int state) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(state, cb::onStateUpdated, ex));
        }

        @Override
        public void onApplyRouting(ResultReceiver isSkipped) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(
                        new ReceiverWrapper<>(isSkipped), cb::onApplyRouting, ex));
        }
        @Override
        public void onNdefRead(ResultReceiver isSkipped) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(
                        new ReceiverWrapper<>(isSkipped), cb::onNdefRead, ex));
        }
        @Override
        public void onEnable(ResultReceiver isAllowed) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(
                        new ReceiverWrapper<>(isAllowed), cb::onEnableRequested, ex));
        }
        @Override
        public void onDisable(ResultReceiver isAllowed) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(
                        new ReceiverWrapper<>(isAllowed), cb::onDisableRequested, ex));
        }
        @Override
        public void onBootStarted() throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(null, (Object input) -> cb.onBootStarted(), ex));
        }
        @Override
        public void onEnableStarted() throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(null, (Object input) -> cb.onEnableStarted(), ex));
        }
        @Override
        public void onDisableStarted() throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(null, (Object input) -> cb.onDisableStarted(), ex));
        }
        @Override
        public void onBootFinished(int status) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(status, cb::onBootFinished, ex));
        }
        @Override
        public void onEnableFinished(int status) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(status, cb::onEnableFinished, ex));
        }
        @Override
        public void onDisableFinished(int status) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(status, cb::onDisableFinished, ex));
        }
        @Override
        public void onTagDispatch(ResultReceiver isSkipped) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(
                        new ReceiverWrapper<>(isSkipped), cb::onTagDispatch, ex));
        }
        @Override
        public void onRoutingChanged(ResultReceiver isSkipped) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(
                            new ReceiverWrapper<>(isSkipped), cb::onRoutingChanged, ex));
        }
        @Override
        public void onHceEventReceived(int action) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(action, cb::onHceEventReceived, ex));
        }

        @Override
        public void onReaderOptionChanged(boolean enabled) throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(enabled, cb::onReaderOptionChanged, ex));
        }

        public void onRoutingTableFull() throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(null,
                            (Object input) -> cb.onRoutingTableFull(), ex));
        }

        @Override
        public void onGetOemAppSearchIntent(List<String> packages, ResultReceiver intentConsumer)
                throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoid2ArgCallback(packages, new ReceiverWrapper<>(intentConsumer),
                            cb::onGetOemAppSearchIntent, ex));
        }

        @Override
        public void onNdefMessage(Tag tag, NdefMessage message,
                                  ResultReceiver hasOemExecutableContent) throws RemoteException {
            mCallbackMap.forEach((cb, ex) -> {
                synchronized (mLock) {
                    final long identity = Binder.clearCallingIdentity();
                    try {
                        ex.execute(() -> cb.onNdefMessage(
                                tag, message, new ReceiverWrapper<>(hasOemExecutableContent)));
                    } catch (RuntimeException exception) {
                        throw exception;
                    } finally {
                        Binder.restoreCallingIdentity(identity);
                    }
                }
            });
        }

        @Override
        public void onLaunchHceAppChooserActivity(String selectedAid,
                                                  List<ApduServiceInfo> services,
                                                  ComponentName failedComponent, String category)
                throws RemoteException {
            mCallbackMap.forEach((cb, ex) -> {
                synchronized (mLock) {
                    final long identity = Binder.clearCallingIdentity();
                    try {
                        ex.execute(() -> cb.onLaunchHceAppChooserActivity(
                                selectedAid, services, failedComponent, category));
                    } catch (RuntimeException exception) {
                        throw exception;
                    } finally {
                        Binder.restoreCallingIdentity(identity);
                    }
                }
            });
        }

        @Override
        public void onLaunchHceTapAgainActivity(ApduServiceInfo service, String category)
                throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoid2ArgCallback(service, category, cb::onLaunchHceTapAgainDialog, ex));
        }

        @Override
        public void onLogEventNotified(OemLogItems item) throws RemoteException  {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoidCallback(item, cb::onLogEventNotified, ex));
        }

        @Override
        public void onExtractOemPackages(NdefMessage message, ResultReceiver packageConsumer)
                throws RemoteException {
            mCallbackMap.forEach((cb, ex) ->
                    handleVoid2ArgCallback(message,
                            new ReceiverWrapper<>(packageConsumer),
                            cb::onExtractOemPackages, ex));
        }

        private <T> void handleVoidCallback(
                T input, Consumer<T> callbackMethod, Executor executor) {
            synchronized (mLock) {
                final long identity = Binder.clearCallingIdentity();
                try {
                    executor.execute(() -> callbackMethod.accept(input));
                } catch (RuntimeException ex) {
                    throw ex;
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
        }

        private <T1, T2> void handleVoid2ArgCallback(
                T1 input1, T2 input2, BiConsumer<T1, T2> callbackMethod, Executor executor) {
            synchronized (mLock) {
                final long identity = Binder.clearCallingIdentity();
                try {
                    executor.execute(() -> callbackMethod.accept(input1, input2));
                } catch (RuntimeException ex) {
                    throw ex;
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
        }

        private <S, T> S handleNonVoidCallbackWithInput(
                S defaultValue, T input, Function<T, S> callbackMethod) throws RemoteException {
            synchronized (mLock) {
                final long identity = Binder.clearCallingIdentity();
                S result = defaultValue;
                try {
                    ExecutorService executor = Executors.newSingleThreadExecutor();
                    FutureTask<S> futureTask = new FutureTask<>(() -> callbackMethod.apply(input));
                    var unused = executor.submit(futureTask);
                    try {
                        result = futureTask.get(
                                OEM_EXTENSION_RESPONSE_THRESHOLD_MS, TimeUnit.MILLISECONDS);
                    } catch (ExecutionException | InterruptedException e) {
                        e.printStackTrace();
                    } catch (TimeoutException e) {
                        Log.w(TAG, "Callback timed out: " + callbackMethod);
                        e.printStackTrace();
                    } finally {
                        executor.shutdown();
                    }
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
                return result;
            }
        }

        private <T> T handleNonVoidCallbackWithoutInput(T defaultValue, Supplier<T> callbackMethod)
                throws RemoteException {
            synchronized (mLock) {
                final long identity = Binder.clearCallingIdentity();
                T result = defaultValue;
                try {
                    ExecutorService executor = Executors.newSingleThreadExecutor();
                    FutureTask<T> futureTask = new FutureTask<>(callbackMethod::get);
                    var unused = executor.submit(futureTask);
                    try {
                        result = futureTask.get(
                                OEM_EXTENSION_RESPONSE_THRESHOLD_MS, TimeUnit.MILLISECONDS);
                    } catch (ExecutionException | InterruptedException e) {
                        e.printStackTrace();
                    } catch (TimeoutException e) {
                        Log.w(TAG, "Callback timed out: " + callbackMethod);
                        e.printStackTrace();
                    } finally {
                        executor.shutdown();
                    }
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
                return result;
            }
        }
    }

    private @CardEmulation.ProtocolAndTechnologyRoute int routeStringToInt(String route) {
        if (route.equals("DH")) {
            return PROTOCOL_AND_TECHNOLOGY_ROUTE_DH;
        } else if (route.startsWith("eSE")) {
            return PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE;
        } else if (route.startsWith("SIM")) {
            return PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC;
        } else if (route.startsWith("NDEF-NFCEE")) {
            return PROTOCOL_AND_TECHNOLOGY_ROUTE_NDEF_NFCEE;
        } else {
            throw new IllegalStateException("Unexpected value: " + route);
        }
    }

    private class ReceiverWrapper<T> implements Consumer<T> {
        private final ResultReceiver mResultReceiver;

        ReceiverWrapper(ResultReceiver resultReceiver) {
            mResultReceiver = resultReceiver;
        }

        @Override
        public void accept(T result) {
            if (result instanceof Boolean) {
                mResultReceiver.send((Boolean) result ? 1 : 0, null);
            } else if (result instanceof Intent) {
                Bundle bundle = new Bundle();
                bundle.putParcelable("intent", (Intent) result);
                mResultReceiver.send(0, bundle);
            } else if (result instanceof List<?> list) {
                if (list.stream().allMatch(String.class::isInstance)) {
                    Bundle bundle = new Bundle();
                    bundle.putStringArray("packageNames",
                            list.stream().map(pkg -> (String) pkg).toArray(String[]::new));
                    mResultReceiver.send(0, bundle);
                }
            }
        }

        @Override
        public Consumer<T> andThen(Consumer<? super T> after) {
            return Consumer.super.andThen(after);
        }
    }
}
