Search code examples
androidbonjourzeroconfnetwork-servicensdmanager

Android NsdManager not able to discover services


I'm running into a problem with Androids NsdManager when following their tutorial Using Network Service Discovery.

I have a few zeroconf/bonjour hardware devices on my network. From my mac I can discover all of them as expected from my terminal with the following dns-sd -Z _my-mesh._tcp.

From my Android app's first run I can flawlessly discover these services using NsdManager. However if I restart the application and try again none of the services are found. onDiscoveryStarted gets called successfully but then nothing else after. While waiting I can confirm from my mac that the services are still successfully there.

I can then turn on my Zeroconf app (on Android) and it will show the services like my mac. When I return to my app I see it immediately receive all the callbacks I expected previously. So I believe something is wrong with my approach, however I'm not sure what. Below is the code I use to discover and resolve services. The view is a giant textview (in a scroll view) I keep writing text to for debugging easier.

import android.annotation.SuppressLint
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView

class MainActivity : AppCompatActivity(),
                     NsdManager.DiscoveryListener {

    private var nsdManager: NsdManager? = null
    private var text: TextView? = null
    private var isResolving = false
    private val services = ArrayList<ServiceWrapper>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        this.text = findViewById(R.id.text)
        this.nsdManager = application.getSystemService(Context.NSD_SERVICE) as NsdManager

    }

    override fun onResume() {
        super.onResume()
        this.nsdManager?.discoverServices("_my-mesh._tcp.", NsdManager.PROTOCOL_DNS_SD, this)
        write("Resume Discovering Services")
    }

    override fun onPause() {
        super.onPause()
        this.nsdManager?.stopServiceDiscovery(this)
        write("Pause Discovering Services")
    }

    override fun onServiceFound(serviceInfo: NsdServiceInfo?) {
        write("onServiceFound(serviceInfo = $serviceInfo))")
        if (serviceInfo == null) {
            return
        }
        add(serviceInfo)
    }

    override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {
        write("onStopDiscoveryFailed(serviceType = $serviceType, errorCode = $errorCode)")
    }

    override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
        write("onStartDiscoveryFailed(serviceType = $serviceType, errorCode = $errorCode)")
    }

    override fun onDiscoveryStarted(serviceType: String?) {
        write("onDiscoveryStarted(serviceType = $serviceType)")
    }

    override fun onDiscoveryStopped(serviceType: String?) {
        write("onDiscoveryStopped(serviceType = $serviceType)")
    }

    override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
        write("onServiceLost(serviceInfo = $serviceInfo)")
    }

    private fun createResolveListener(): NsdManager.ResolveListener {
        return object : NsdManager.ResolveListener {
            override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
                write("onResolveFailed(serviceInfo = $serviceInfo, errorCode = $errorCode)")
                isResolving = false
                resolveNext()
            }

            override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
                write("onServiceResolved(serviceInfo = $serviceInfo)")
                if (serviceInfo == null) {
                    return
                }
                for (servicewrapper in services) {
                    if (servicewrapper.serviceInfo.serviceName == serviceInfo.serviceName) {
                        servicewrapper.resolve(serviceInfo)
                    }
                }
                isResolving = false
                resolveNext()
            }
        }
    }

    @SuppressLint("SetTextI18n")
    private fun write(text: String?) {
        this.text?.let {
            it.post({
                        it.text = it.text.toString() + "\n" + text + "\n"
                    })
        }
    }

    fun add(serviceInfo: NsdServiceInfo) {
        for (servicewrapper in services) {
            if (servicewrapper.serviceInfo.serviceName == serviceInfo.serviceName) {
                return
            }
        }
        services.add(ServiceWrapper(serviceInfo))
        resolveNext()
    }

    @Synchronized
    fun resolveNext() {
        if (isResolving) {
            return
        }
        isResolving = true
        for (servicewrapper in services) {
            if (servicewrapper.isResolved) {
                continue
            }
            write("resolving")
            this.nsdManager?.resolveService(servicewrapper.serviceInfo, createResolveListener())
            return
        }
        isResolving = false

    }

    inner class ServiceWrapper(var serviceInfo: NsdServiceInfo) {

        var isResolved = false

        fun resolve(serviceInfo: NsdServiceInfo) {
            isResolved = true
            this.serviceInfo = serviceInfo


        }

    }


}

Solution

  • Better late than never. Did not realize other people were having this issue too until now.

    What we discovered was some routers were blocking or not correctly forwarding the packets back and forth. Our solution to this was using wire shark to detect what other popular apps were doing to get around the issue. Androids NsdManager has limited customizability so it required manually transmitting the packet over a MulticastSocket.

    interface NsdDiscovery {
    
    suspend fun startDiscovery()
    suspend fun stopDiscovery()
    fun setListener(listener: Listener?)
    fun isDiscovering(): Boolean
    
    interface Listener {
    
        fun onServiceFound(ip:String, local:String)
        fun onServiceLost(event: ServiceEvent)
    
    }
    
    }
    
    @Singleton
    class ManualNsdDiscovery @Inject constructor()
    : NsdDiscovery {
    
    //region Fields
    
    private val isDiscovering = AtomicBoolean(false)
    
    private var socketManager: SocketManager? = null
    
    private var listener: WeakReference<NsdDiscovery.Listener> = WeakReference<NsdDiscovery.Listener>(null)
    
    //endregion
    
    //region NsdDiscovery
    
    override suspend fun startDiscovery() = withContext(Dispatchers.IO) {
        if (isDiscovering()) return@withContext
        [email protected](true)
        val socketManager = SocketManager()
        socketManager.start()
        [email protected] = socketManager
    
    }
    
    override suspend fun stopDiscovery() = withContext(Dispatchers.IO) {
        if (!isDiscovering()) return@withContext
        [email protected]?.stop()
        [email protected] = null
        [email protected](false)
    }
    
    override fun setListener(listener: NsdDiscovery.Listener?) {
        this.listener = WeakReference<NsdDiscovery.Listener>(listener)
    }
    
    @Synchronized
    override fun isDiscovering(): Boolean {
        return this.isDiscovering.get()
    }
    
    //endregion
    
    private inner class SocketManager {
    
        //region Fields
    
        private val group = InetAddress.getByName("224.0.0.251")
                            ?: throw IllegalStateException("Can't setup group")
    
        private val incomingNsd = IncomingNsd()
    
        private val outgoingNsd = OutgoingNsd()
    
        //endregion
    
        //region Constructors
    
    
        //endregion
    
        //region Methods
    
        suspend fun start() {
            this.incomingNsd.startListening()
            this.outgoingNsd.send()
        }
    
        fun stop() {
            this.incomingNsd.stopListening()
        }
    
    
        //endregion
    
    
        private inner class OutgoingNsd {
    
            //region Fields
    
            private val socketMutex = Mutex()
    
            private var socket = MulticastSocket(5353)
    
            suspend fun setUpSocket() {
                this.socketMutex.withLock {
                    try {
                        this.socket = MulticastSocket(5353)
                        this.socket.reuseAddress = true
                        this.socket.joinGroup(group)
                    } catch (e: SocketException) {
                        return
                    }
                }
            }
    
            suspend fun tearDownSocket() {
                this.socketMutex.withLock {
                    [email protected]()
                }
            }
    
            //ugly code but here is the packet
            private val bytes = byteArrayOf(171.toByte(), 205.toByte(), 1.toByte(), 32.toByte(),
                                            0.toByte(), 1.toByte(), 0.toByte(), 0.toByte(),
                                            0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(),
                                            9.toByte(), 95.toByte(), 101.toByte(), 118.toByte(),
                                            97.toByte(), 45.toByte(), 109.toByte(), 101.toByte(),
                                            115.toByte(), 104.toByte(), 4.toByte(), 95.toByte(),
                                            116.toByte(), 99.toByte(), 112.toByte(), 5.toByte(),
                                            108.toByte(), 111.toByte(), 99.toByte(), 97.toByte(),
                                            108.toByte(), 0.toByte(), 0.toByte(), 12.toByte(),
                                            0.toByte(), 1.toByte())
    
    
            private val outPacket = DatagramPacket(bytes,
                                                   bytes.size,
                                                   [email protected],
                                                   5353)
    
            //endregion
            //region Methods
    
            @Synchronized
            suspend fun send() {
                withContext(Dispatchers.Default) {
                    setUpSocket()
                    try {
                        [email protected]([email protected])
                        delay(1500L)
                        tearDownSocket()
                    } catch (e: Exception) {
    
                    }
    
                }
            }
    
            //endregion
    
        }
    
        private inner class IncomingNsd {
    
            //region Fields
    
    
            private val isRunning = AtomicBoolean(false)
    
            private var socket = MulticastSocket(5353)
    
            //endregion
    
            //region Any
    
            fun setUpSocket() {
                try {
                    this.socket = MulticastSocket(5353)
                    this.socket.reuseAddress = true
                    this.socket.joinGroup(group)
                } catch (e: SocketException) {
    
                } catch (e: BindException) {
    
                }
            }
    
            fun run() {
                GlobalScope.launch(Dispatchers.Default) {
                    setUpSocket()
                    try {
                        while ([email protected]()) {
                            val bytes = ByteArray(4096)
                            val inPacket = DatagramPacket(bytes, bytes.size)
                            [email protected](inPacket)
                            val incoming = DNSIncoming(inPacket)
                            for (answer in incoming.allAnswers) {
                                if (answer.key.contains("_my_mesh._tcp")) {
                                    [email protected]()?.onServiceFound(answer.recordSource.hostAddress, answer.name)
                                    return@launch
                                }
                            }
    
                        }
                        [email protected]()
                    } catch (e: Exception) {
    
                    }
                }
            }
    
            //endregion
    
            //region Methods
    
            @Synchronized
            fun startListening() {
                if (this.isRunning.get()) {
                    return
                }
                this.isRunning.set(true)
                run()
            }
    
            @Synchronized
            fun stopListening() {
                if (!this.isRunning.get()) {
                    return
                }
                this.isRunning.set(false)
            }
    
            //endregion
    
        }
    
    }
    
    }