Using Android’s Network Service Discovery to connect to a device on the local area network

Marc Rooding
Ramblings of a Dutch dev
6 min readDec 4, 2019

--

Image courtesy of Pixabay.com

Introduction

In this first article of a series about my learnings around the Android ecosystem, I’d like to look into what Android offers in terms of discovering devices and services on your local network. I’ll explain what underlying technologies are used and how you can use Android’s Network Service Discovery to discover devices on your network.

If you’re not interested in why I started using the Android ecosystem, feel free to skip the background story.

Background story

A few months ago, I bought a new home cinema receiver from NAD. Compared to the old receiver that I had, this one came with a BluOS streaming module. BluOS is a competitor of, for example, Sonos, which allows you to connect to your local network and control the music playing on your receiver.

A friend of mine has had the same receiver already, and he was happily using a fan-made iOS application to connect to and control the receiver. To my disappointment, there was no Android equivalent available.

I haven’t been involved with Android since 2010, where I was working on a proof-of-concept application in Android, so I saw this as a fun opportunity to see if I could build an equivalent application for Android.

After about a month of fiddling, I released it to the Play store. Over the past months working on the app, I realised that there’s a lot of knowledge I gained. Some of that, I’ll be sharing back so that others can benefit from it.

Figuring out how to connect to the device

So, I want to connect to a NAD receiver on my local area network. How would we get started here?

Although the NAD documentation on how to interface with the device was near to non-existent I did find some documentation on one of the older models. This document described the RS-232 specification which was exposed over ethernet. It mentioned that you could connect to the receiver using a raw TCP/IP socket connection on port 23. That’s great to know, but you still need to know the IP address of the device before connecting. For this, the device makes use of DNS-SD over mDNS.

DNS-SD over mDNS

DNS Service Discovery, or DNS-SD, is a way of using standard DNS programming interfaces, servers and packet formats to browse the network for services.

Multicast DNS, or mDNS, is a protocol that resolves hostnames to IP addresses within small networks that do not have a dedicated local name server. Like DNS-SD, it also uses the same DNS programming interfaces, servers and packet formats.

In simple terms, devices can make themselves known to the local area network by broadcasting who they are and what service they expose. DNS-SD capable clients can receive these messages and discover services on the same local network.

For example, a device that supports Telnet over TCP/IP will broadcast _telnet._tcp.. The syntax is standardised and is as follows: _<service>._<protocol>.

I was very curious to inspect what I would be able to find on my local network. For macOS, there’s a free application available, called Discovery. This application lets you see all devices broadcasting on your local network in a graphical interface

Broadcasting devices on my local network

As you can see from the screenshot above, my NAD receiver is listed as _telnet._tcp..

Network Service Discovery on Android

Network Service Discovery, or NSD, implements the DNS-SD mechanism, which allows your Android application to request services by specifying the type of service (like _telnet.tcp_.).

The NSD API offered by Android consists of 3 steps:

  1. Discovering services
  2. Resolving services
  3. Registering your discovery listener

Let’s dive into the implementation.

Implementing NSD

The implementation revolves around a NSD_SERVICE system service. Inside a class that has access to the android.app.Application class, you can get a reference to the NsdManager that I require to register our listeners:

val nsdManager = getSystemService(Context.NSD_SERVICE) as NsdManager

Besides the nsdManager, I’ll create a variable which references the service type that I’m interested in:

val SERVICE_TYPE = "_telnet._tcp."

Resolving services

When I, later on, move to the implementation of the discovery listener, I’ll need to have an instance of NsdManager.ResolveListener available. This resolve listener is used to resolve the service when it found by the discovery listener. When I find the service I’m interested in, I call the NsdManager.resolveService(service, resolveListener) method and pass in the service found, and the resolve listener that I’d like to use for receiving callbacks.

A resolve listener only has 2 callbacks available, on for failure and one for success:

private val resolveListener = object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
...
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
// do whatever I want with the resolved service
}
}

When a service is resolved successfully, a NsdServiceInfo class is passed to the callback method. This class contains all the necessary information, like hostname and port, to set up a socket connection to the service.

Discovering Services

Discovering services is done by creating an implementation of NsdManager.DiscoveryListener. This interface contains 6 methods that you’ll need to implement which manage the discovery lifecycle:

public interface DiscoveryListener {
public void onStartDiscoveryFailed(String serviceType, int errorCode);
public void onStopDiscoveryFailed(String serviceType, int errorCode); public void onDiscoveryStarted(String serviceType); public void onDiscoveryStopped(String serviceType); public void onServiceFound(NsdServiceInfo serviceInfo); public void onServiceLost(NsdServiceInfo serviceInfo);
}

To keep it concise, I’ll cover the 2 most important ones, namely onServiceFound and onServiceLost.

As indicated by the name, onServiceFound is called when a service matching the type that you indicate is found:

override fun onServiceFound(service: NsdServiceInfo) {
when {
service.serviceType != SERVICE_TYPE ->
...
service.serviceName.contains("NAD") ->
nsdManager.resolveService(service, resolveListener)
}
}

In the method, I log any mismatches in the type that I’m interested in and the type of service that’s found. More importantly, I also check whether the service’s name contains NAD. In this case, I have the match that I’m looking for. Using the nsdManager reference I created earlier on, I can resolve the service with the resolveListener that I created earlier on.

The onServiceLost method is called when a service which was previously found is lost:

override fun onServiceLost(service: NsdServiceInfo) {
if (service.serviceType != SERVICE_TYPE) {
...
} else {
// do whatever I want with the lost service
}
}

If you’re doing internal bookkeeping to keep track of the services that you found, this would be the callback that you’d need to remove the service which was lost.

For the sake of completeness, the complete discovery listener would look something like this:

private val discoveryListener = object : NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(regType: String) {
...
}
override fun onServiceFound(service: NsdServiceInfo) {
...
when {
service.serviceType != SERVICE_TYPE ->
...
service.serviceName.contains("NAD") ->
nsdManager.resolveService(service, resolveListener)
}
}
override fun onServiceLost(service: NsdServiceInfo) {
if (service.serviceType != type) {
...
} else {
serviceInfoSubject.onNext(Option.empty())
}
}
override fun onDiscoveryStopped(serviceType: String) {
...
}
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
nsdManager.stopServiceDiscovery(this)
}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
nsdManager.stopServiceDiscovery(this)
}
}

Registering your discovery listener

Last but not least, I have to register a discovery listener with the underlying Android system:

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

I tell it which service type to discover, which protocol I’d like to use and last but not least, which discovery listener to use.

Note: It’s not possible to re-use the same discovery listener for multiple service types.

Conclusion

In this article, I’ve gone through the Android API for discovering and resolving services on a local network. I’ve shown you what underlying technology is used and how to implement it yourself.

In the next article, I’ll dive into the connection and communication lifecycle between the Android application and the NAD receiver, and how I’ve utilised RXJava Reactive streams to accomplish this.

--

--