Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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")
    }

}
like image 460
DarkJediNinja Avatar asked Sep 14 '19 23:09

DarkJediNinja


2 Answers

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()
    }

}
like image 196
DarkJediNinja Avatar answered Nov 10 '22 04:11

DarkJediNinja


I just run into the same issue... And boy how I hate Android APIs sometimes...

Anyways, my solution is still far from perfect, but at least a bit simpler.

I'm basically using a java.util.concurrent.Semaphore to hold back any further resolutions until the current one finishes.

Hope this helps someone. Cheers!

val semaphore = Semaphore(1)

nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, this)

override fun onServiceFound(service: NsdServiceInfo) {
  //
  // Do some fancy logic to filter out Services you don't need...
  //
  thread {
    semaphore.acquire()
    nsdManager.resolveService(service, this)
  }
}

override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
  //
  // Handle errors...
  //
  semaphore.release()
}

override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
  //
  // Service is resolved, do something with it...
  //
  semaphore.release()
}

like image 32
Grzegorz D. Avatar answered Nov 10 '22 05:11

Grzegorz D.