NsdManager discovers, immediately loses and re-discovers same service after WiFi is re-enabled on Android phone

750 Views Asked by At

In my Android app I have to show a list of available services on the network published by another machine (RPi 3B con Raspbian Stretch) using avahi 0.6.32 (Bonjour/zeroconf daemon for Linux). I obtain the list on Android phone using NsdManager. But, during testing, I'm getting a strange behavior: when I switch WiFi off and back on in the phone, most of the times the services are discovered, then immediately lost and then rediscovered (instead of just discovered once) and all of this in less than a second.

This causes the list of services to briefly appear on screen, then disappear and finally reappear almost immediately, but it's still very noticeable. And it forces the services to be discovered and resolved twice. As I expect to have lots of phones connecting to several services in the same LAN, I want to avoid overloading the network.

I'm not sure if I'm doing something wrong or it's just the way NsdManager works on Android. To reduce possible sources of problem, I commented out the lines that resolve the services (leaving only the log messages) but the problem persisted (more then half of the times).

How can I solve it?

Sample extract from Logcat:

2019-09-26 04:33:50.262 27300-27420/com.example.myapp D/NsdHelper$initializeDiscoveryListener: Service discovery success: name: MyService-1490247, type: _mytype._tcp., host: null, port: 0, txtRecord:

2019-09-26 04:33:50.879 27300-27420/com.example.myapp D/NsdHelper$initializeDiscoveryListener: Service lost: name: MyService-1490247, type: _mytype._tcp., host: null, port: 0, txtRecord:

2019-09-26 04:33:50.970 27300-27420/com.example.myapp D/NsdHelper$initializeDiscoveryListener: Service discovery success: name: MyService-1490247, type: _mytype._tcp., host: null, port: 0, txtRecord:

I'm testing on a Samsung Note 8 with Android O. I tried with 2 different WiFi routers and the behavior is the same.

I'm using the following NsdHelper class (in Kotlin):

    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 = "_mytype._tcp."
            // Services' Names must start with this
            const val NSD_SERVICE_NAME: String = "MyService-"
        }

        // 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() {

            Timber.d("Initialize DiscoveryListener")

            // 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
                    synchronized(pendingNsdServices) {
                        val 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) {
                        val 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.d("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() {

            Timber.d("Initialize ResolveListener")

            resolveListener =  object : NsdManager.ResolveListener {

                override fun onServiceResolved(service: NsdServiceInfo) {
                    Timber.d("Service 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) {
                Timber.d("stopDiscovery() called")
                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)
    }

On the init block of the View Model, I start the service discovery:

    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
                // Update list of available services
                updateServicesList()
            }

            override fun onNsdServiceLost(service: NsdServiceInfo) {
                // A network service is no longer available

                // Update list of available services
                updateServicesList()

            }
        }

        // Block that is run when the view model is created
        init {
            Timber.d("init block called")

            // 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() {
            Timber.d("onCleared called")
            nsdHelper?.stopDiscovery()
            super.onCleared()
        }

        private fun updateServicesList() {
            // Put the logic here to show the services on screen
            return
        }
    }

Note: Timber is a logging utility, almost a direct replacement for standard Log commands, but easier to use.

0

There are 0 best solutions below