androidbonjourzeroconfnsdnsdmanager

NSdManager ResolveListener Error Code 3: Failure Already active


I'm using NsdManager in an Android App to discover NSD services published by another device also developed by me. I only do service discovery on Android App (no service registration needed on this side). There are several instances of the same type of service published at the same time on the network.

I started using the sample code provided by Google (https://developer.android.com/training/connect-devices-wirelessly/nsd) but I had fatal errors due to reusing the same resolver object at the same time for more than one service resolution. Then I found several people suggesting to create a new resolver object each time (like in Listener already in use (Service Discovery)).

I did this and the fatal error was replaced by a Resolve Failure error code 3 that meant that the resolve process was active. Better than before, but only the first service was resolved and the rest was ignored due to this failure.

Then I found a person suggesting to give a special treatment to Error Code 3 by resending the resolve request recursively until it eventually becomes resolved ( NSNetworkManager.ResolveListener messages Android).

I implemented this solution in Kotlin and it kind of works but I'm not really satisfied because:

  1. I believe that I'm creating a lot of additional Resolver objects and I'm not sure if they are later garbage collected or not.
  2. I'm retrying several times in a loop, maybe causing additional and unnecessary burden on the device and the network. Not sure if I should add a short sleep before invoking service resolution again.
  3. If there is some network problem, the program may try thousand of times to resolve the same service instead of just abandoning the resolution and waiting for the service to be discovered again.

The people of RxBonjour2 have come with a more complex and robust solution but it's too complex for me to follow it: https://github.com/mannodermaus/RxBonjour/blob/2.x/rxbonjour-drivers/rxbonjour-driver-nsdmanager/src/main/kotlin/de/mannodermaus/rxbonjour/drivers/nsdmanager/NsdManagerDiscoveryEngine.kt

I feel frustrated that Google's official examples do not handle these problems correctly. The nsd_chat sample uses a single resolver object and fails when more than one service with the same type is published at the same type on the network.

Can you suggest a better solution? Or any improvements to my code below?

import android.app.Application
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import androidx.lifecycle.AndroidViewModel
import timber.log.Timber


class ViewModel(application: Application) : AndroidViewModel(application) {

    // Get application context
    private val myAppContext: Context = getApplication<Application>().applicationContext

    // Declare DNS-SD related variables for service discovery
    var nsdManager: NsdManager? = null
    private var discoveryListener: NsdManager.DiscoveryListener? = null

    // Constructor for the View Model that is run when the view model is created
    init {

        // Initialize DNS-SD service discovery
        nsdManager = myAppContext.getSystemService(Context.NSD_SERVICE) as NsdManager?

        initializeDiscoveryListener()

        // Start looking for available services in the network
        nsdManager?.discoverServices(NSD_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)

    }

    // Instantiate DNS-SD discovery listener
    // used to discover available Sonata audio servers on the same network
    private fun initializeDiscoveryListener() {

        // Instantiate a new DiscoveryListener
        discoveryListener = object : NsdManager.DiscoveryListener {

            override fun onDiscoveryStarted(regType: String) {
                // Called as soon as service discovery begins.
                Timber.d("Service discovery started: $regType")
            }

            override fun onServiceFound(service: NsdServiceInfo) {
                // A service was found! Do something with it
                Timber.d("Service discovery success: $service")
                when {
                    service.serviceType != NSD_SERVICE_TYPE ->
                        // Service type is not the one we are looking for
                        Timber.d("Unknown Service Type: ${service.serviceType}")
                    service.serviceName.contains(NSD_SERVICE_NAME) ->
                        // Both service type and service name are the ones we want
                        // Resolve the service to get all the details
                        startResolveService(service)
                    else ->
                        // Service type is ours but not the service name
                        // Log message but do nothing else
                        Timber.d("Unknown Service Name: ${service.serviceName}")
                }
            }

            override fun onServiceLost(service: NsdServiceInfo) {
                onNsdServiceLost(service)
            }

            override fun onDiscoveryStopped(serviceType: String) {
                Timber.i("Discovery stopped: $serviceType")
            }

            override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
                Timber.e("Start Discovery failed: Error code: $errorCode")
                nsdManager?.stopServiceDiscovery(this)
            }

            override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
                Timber.e("Stop Discovery failed: Error code: $errorCode")
                nsdManager?.stopServiceDiscovery(this)
            }
        }
    }

    fun startResolveService(service: NsdServiceInfo) {

        val newResolveListener =  object : NsdManager.ResolveListener {

            override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
                // Called when the resolve fails. Use the error code to determine action.
                when (errorCode) {
                    NsdManager.FAILURE_ALREADY_ACTIVE -> {
                        // Resolver was busy
                        Timber.d("Resolve failed: $serviceInfo - Already active")
                        // Just try again...
                        startResolveService(serviceInfo)
                    }
                    else ->
                        Timber.e("Resolve failed: $serviceInfo - Error code: $errorCode")
                }
            }

            override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
                onNsdServiceResolved(serviceInfo)
            }
        }

        nsdManager?.resolveService(service, newResolveListener)
    }

    companion object {

        // We'll only search for NDS services of this type
        const val NSD_SERVICE_TYPE: String = "_servicetype._tcp."
        // and whose names start like this
        const val NSD_SERVICE_NAME: String = "ServiceName-"
    }

    override fun onCleared() {
        try {
            nsdManager?.stopServiceDiscovery(discoveryListener)
        } catch (ignored: Exception) {
            // "Service discovery not active on discoveryListener",
            // thrown if starting the service discovery was unsuccessful earlier
        }
        Timber.d("onCleared called")
        super.onCleared()
    }

    fun onNsdServiceResolved(serviceInfo: NsdServiceInfo) {
        // Logic to handle a new service
        Timber.d("Resolve Succeeded: $serviceInfo")
    }

    fun onNsdServiceLost(service: NsdServiceInfo) {
        // Logic to handle when the network service is no longer available
        Timber.d("Service lost: $service")
    }

}

Solution

  • I solved the problem by:

    1. Creating a thread-safe queue to store the services pending to be resolved
    2. Using a thread-safe list to store the list of already resolved services
    3. Using an Atomic Boolean flag to see when the ResolveListener is busy

    To make the solution more generic, I built an NdsHelper abstract class. It has 2 functions that must be overridden: onNsdServiceResolved(NsdServiceInfo) and onNsdServiceLost(NsdServiceInfo).

    I'm using Timber for logging messages but you can replace them by the standard Log function.

    This is the NsdHelper class (Kotlin code):

    import android.content.Context
    import android.net.nsd.NsdManager
    import android.net.nsd.NsdServiceInfo
    import timber.log.Timber
    import java.util.*
    import java.util.concurrent.ConcurrentLinkedQueue
    import java.util.concurrent.atomic.AtomicBoolean
    import kotlin.collections.ArrayList
    
    abstract class NsdHelper(val context: Context) {
    
        // Declare DNS-SD related variables for service discovery
        val nsdManager: NsdManager? = context.getSystemService(Context.NSD_SERVICE) as NsdManager?
        private var discoveryListener: NsdManager.DiscoveryListener? = null
        private var resolveListener: NsdManager.ResolveListener? = null
        private var resolveListenerBusy = AtomicBoolean(false)
        private var pendingNsdServices = ConcurrentLinkedQueue<NsdServiceInfo>()
        var resolvedNsdServices: MutableList<NsdServiceInfo> = Collections.synchronizedList(ArrayList<NsdServiceInfo>())
    
        companion object {
    
            // Type of services to look for
            const val NSD_SERVICE_TYPE: String = "_myservicetype._tcp."
            // Services' Names must start with this
            const val NSD_SERVICE_NAME: String = "MyServiceName-"
        }
    
        // Initialize Listeners
        fun initializeNsd() {
            // Initialize only resolve listener
            initializeResolveListener()
        }
    
        // Instantiate DNS-SD discovery listener
        // used to discover available Sonata audio servers on the same network
        private fun initializeDiscoveryListener() {
    
            // Instantiate a new DiscoveryListener
            discoveryListener = object : NsdManager.DiscoveryListener {
    
                override fun onDiscoveryStarted(regType: String) {
                    // Called as soon as service discovery begins.
                    Timber.d("Service discovery started: $regType")
                }
    
                override fun onServiceFound(service: NsdServiceInfo) {
                    // A service was found! Do something with it
                    Timber.d("Service discovery success: $service")
    
                    if ( service.serviceType == NSD_SERVICE_TYPE &&
                            service.serviceName.startsWith(NSD_SERVICE_NAME) ) {
                        // Both service type and service name are the ones we want
                        // If the resolver is free, resolve the service to get all the details
                        if (resolveListenerBusy.compareAndSet(false, true)) {
                            nsdManager?.resolveService(service, resolveListener)
                        }
                        else {
                            // Resolver was busy. Add the service to the list of pending services
                            pendingNsdServices.add(service)
                        }
                    }
                    else {
                        // Not our service. Log message but do nothing else
                        Timber.d("Not our Service - Name: ${service.serviceName}, Type: ${service.serviceType}")
                    }
                }
    
                override fun onServiceLost(service: NsdServiceInfo) {
                    Timber.d("Service lost: $service")
    
                    // If the lost service was in the queue of pending services, remove it
                    var iterator = pendingNsdServices.iterator()
                    while (iterator.hasNext()) {
                        if (iterator.next().serviceName == service.serviceName)
                            iterator.remove()
                    }
    
                    // If the lost service was in the list of resolved services, remove it
                    synchronized(resolvedNsdServices) {
                        iterator = resolvedNsdServices.iterator()
                        while (iterator.hasNext()) {
                            if (iterator.next().serviceName == service.serviceName)
                                iterator.remove()
                        }
                    }
    
                    // Do the rest of the processing for the lost service
                    onNsdServiceLost(service)
                }
    
                override fun onDiscoveryStopped(serviceType: String) {
                    Timber.i("Discovery stopped: $serviceType")
                }
    
                override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
                    Timber.e("Start Discovery failed: Error code: $errorCode")
                    stopDiscovery()
                }
    
                override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
                    Timber.e("Stop Discovery failed: Error code: $errorCode")
                    nsdManager?.stopServiceDiscovery(this)
                }
            }
        }
    
        // Instantiate DNS-SD resolve listener to get extra information about the service
        private fun initializeResolveListener() {
            resolveListener =  object : NsdManager.ResolveListener {
    
                override fun onServiceResolved(service: NsdServiceInfo) {
                    Timber.d("Resolve Succeeded: $service")
    
                    // Register the newly resolved service into our list of resolved services
                    resolvedNsdServices.add(service)
    
                    // Process the newly resolved service
                    onNsdServiceResolved(service)
    
                    // Process the next service waiting to be resolved
                    resolveNextInQueue()
                }
    
                override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
                    // Called when the resolve fails. Use the error code to debug.
                    Timber.e("Resolve failed: $serviceInfo - Error code: $errorCode")
    
                    // Process the next service waiting to be resolved
                    resolveNextInQueue()
                }
            }
        }
    
        // Start discovering services on the network
        fun discoverServices() {
            // Cancel any existing discovery request
            stopDiscovery()
    
            initializeDiscoveryListener()
    
            // Start looking for available audio channels in the network
            nsdManager?.discoverServices(NSD_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
        }
    
        // Stop DNS-SD service discovery
        fun stopDiscovery() {
            if (discoveryListener != null) {
                try {
                    nsdManager?.stopServiceDiscovery(discoveryListener)
                } finally {
                }
                discoveryListener = null
            }
        }
    
        // Resolve next NSD service pending resolution
        private fun resolveNextInQueue() {
            // Get the next NSD service waiting to be resolved from the queue
            val nextNsdService = pendingNsdServices.poll()
            if (nextNsdService != null) {
                // There was one. Send to be resolved.
                nsdManager?.resolveService(nextNsdService, resolveListener)
            }
            else {
                // There was no pending service. Release the flag
                resolveListenerBusy.set(false)
            }
        }
    
        // Function to be overriden with custom logic for new service resolved
        abstract fun onNsdServiceResolved(service: NsdServiceInfo)
    
        // Function to be overriden with custom logic for service lost
        abstract fun onNsdServiceLost(service: NsdServiceInfo)
    }
    

    And this is how to use it from a ViewModel (or from an activity or fragment, if you change from where to invoke the different helper methods):

    import android.app.Application
    import android.content.Context
    import android.content.Intent
    import android.net.ConnectivityManager
    import android.net.nsd.NsdServiceInfo
    import androidx.lifecycle.AndroidViewModel
    import timber.log.Timber
    import java.util.*
    
    
    class MyViewModel(application: Application) : AndroidViewModel(application) {
    
        // Get application context
        private val myAppContext: Context = getApplication<Application>().applicationContext
    
        // Declare NsdHelper object for service discovery
        private val nsdHelper: NsdHelper? = object : NsdHelper(myAppContext) {
    
            override fun onNsdServiceResolved(service: NsdServiceInfo) {
                // A new network service is available
    
                // Put your custom logic here!!!
    
            }
    
            override fun onNsdServiceLost(service: NsdServiceInfo) {
                // A network service is no longer available
    
                // Put your custom logic here!!!
    
            }
        }
    
        // Block that is run when the view model is created
        init {
    
            // Initialize DNS-SD service discovery
            nsdHelper?.initializeNsd()
    
            // Start looking for available audio channels in the network
            nsdHelper?.discoverServices()
    
        }
    
        // Called when the view model is destroyed
        override fun onCleared() {
            nsdHelper?.stopDiscovery()
            Timber.d("onCleared called")
            super.onCleared()
        }
    
    }