Last post we set up a Android Network Service Discovery server that will receive text messages from our client. Now we are ready to create the client that we will use to create the text messages and send to the server.
When creating the project choose Sdk version 16, Jelly Bean 4.1.
or
Edit the Manifest first to avoid errors when creating the MainActivity class.
Replace minimum with android:minSdkVersion=”16″
and add
1
2
|
|
This will all be done in one file for simplicity.
import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import org.json.JSONException; import org.json.JSONObject; import android.app.Activity; import android.content.Context; import android.net.nsd.NsdManager; import android.net.nsd.NsdServiceInfo; import android.net.wifi.WifiManager; import android.os.AsyncTask; import android.os.Bundle; import android.text.format.Formatter; import android.util.Log; import android.view.Menu; import android.widget.Toast; public class MainActivity extends Activity { private String SERVICE_NAME = "Client Device"; private String SERVICE_TYPE = "_letstalk._tcp."; private InetAddress hostAddress; private int hostPort; private NsdManager mNsdManager; private int SocketServerPort = 6000; private static final String REQUEST_CONNECT_CLIENT = "request-connect-client"; private static final String TAG = "NSDClient"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mNsdManager = (NsdManager) getSystemService(Context.NSD_SERVICE); mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener); } NsdManager.DiscoveryListener mDiscoveryListener = new NsdManager.DiscoveryListener() { // Called as soon as service discovery begins. @Override public void onDiscoveryStarted(String regType) { Log.d(TAG, "Service discovery started"); } @Override public void onServiceFound(NsdServiceInfo service) { // A service was found! Do something with it. Log.d(TAG, "Service discovery success : " + service); Log.d(TAG, "Host = "+ service.getServiceName()); Log.d(TAG, "port = " + String.valueOf(service.getPort())); if (!service.getServiceType().equals(SERVICE_TYPE)) { // Service type is the string containing the protocol and // transport layer for this service. Log.d(TAG, "Unknown Service Type: " + service.getServiceType()); } else if (service.getServiceName().equals(SERVICE_NAME)) { // The name of the service tells the user what they'd be // connecting to. It could be "Bob's Chat App". Log.d(TAG, "Same machine: " + SERVICE_NAME); } else { Log.d(TAG, "Diff Machine : " + service.getServiceName()); // connect to the service and obtain serviceInfo mNsdManager.resolveService(service, mResolveListener); } } @Override public void onServiceLost(NsdServiceInfo service) { // When the network service is no longer available. // Internal bookkeeping code goes here. Log.e(TAG, "service lost" + service); } @Override public void onDiscoveryStopped(String serviceType) { Log.i(TAG, "Discovery stopped: " + serviceType); } @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { Log.e(TAG, "Discovery failed: Error code:" + errorCode); mNsdManager.stopServiceDiscovery(this); } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { Log.e(TAG, "Discovery failed: Error code:" + errorCode); mNsdManager.stopServiceDiscovery(this); } }; NsdManager.ResolveListener mResolveListener = new NsdManager.ResolveListener() { @Override public void onServiceResolved(NsdServiceInfo serviceInfo) { Log.d(TAG, "Resolve Succeeded. " + serviceInfo); if (serviceInfo.getServiceName().equals(SERVICE_NAME)) { Log.d(TAG, "Same IP."); return; } // Obtain port and IP hostPort = serviceInfo.getPort(); hostAddress = serviceInfo.getHost(); /* Once the client device resolves the service and obtains * server's ip address, connect to the server and send data */ connectToHost(); } @Override public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { // Called when the resolve fails. Use the error code to debug. Log.e(TAG, "Resolve failed " + errorCode); Log.e(TAG, "serivce = " + serviceInfo); } }; private void connectToHost() { if (hostAddress == null) { Log.e(TAG, "Host Address is null"); return; } String ipAddress = getLocalIpAddress(); JSONObject jsonData = new JSONObject(); try { jsonData.put("request", REQUEST_CONNECT_CLIENT); jsonData.put("ipAddress", ipAddress); } catch (JSONException e) { e.printStackTrace(); Log.e(TAG, "can't put request"); return; } new SocketServerTask().execute(jsonData); } private String getLocalIpAddress() { WifiManager wm = (WifiManager) getSystemService(WIFI_SERVICE); String ip = Formatter.formatIpAddress(wm.getConnectionInfo().getIpAddress()); return ip; } private class SocketServerTask extends AsyncTask<JSONObject, Void, Void> { private JSONObject jsonData; private boolean success; @Override protected Void doInBackground(JSONObject... params) { Socket socket = null; DataInputStream dataInputStream = null; DataOutputStream dataOutputStream = null; jsonData = params[0]; try { // Create a new Socket instance and connect to host socket = new Socket(hostAddress, SocketServerPort); dataOutputStream = new DataOutputStream( socket.getOutputStream()); dataInputStream = new DataInputStream(socket.getInputStream()); // transfer JSONObject as String to the server dataOutputStream.writeUTF(jsonData.toString()); Log.i(TAG, "waiting for response from host"); // Thread will wait till server replies String response = dataInputStream.readUTF(); if (response != null && response.equals("Connection Accepted")) { success = true; } else { success = false; } } catch (IOException e) { e.printStackTrace(); success = false; } finally { // close socket if (socket != null) { try { Log.i(TAG, "closing the socket"); socket.close(); } catch (IOException e) { e.printStackTrace(); } } // close input stream if (dataInputStream != null) { try { dataInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } // close output stream if (dataOutputStream != null) { try { dataOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } @Override protected void onPostExecute(Void result) { if (success) { Toast.makeText(MainActivity.this, "Connection Done", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this, "Unable to connect", Toast.LENGTH_SHORT).show(); } } } protected void onPuase() { if (mNsdManager != null) { mNsdManager.stopServiceDiscovery(mDiscoveryListener); } super.onPause(); } @Override protected void onResume() { super.onResume(); if (mNsdManager != null) { mNsdManager.discoverServices( SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener); } } @Override protected void onDestroy() { if (mNsdManager != null) { mNsdManager.stopServiceDiscovery(mDiscoveryListener); } super.onDestroy(); } }
SERVICE_NAME is a constant that we use to set the device name, the Google example doesn’t use these it picks the name for you which is the service name if more than one device has the same service name, which it will, the 2nd device has a (1) added to it and the 3rd would have (2) added to it and so on. Not sure if this is what made the Google version so flaky so I don’t use it, I give the server a name and the client a name, and use that.
SERVICE_TYPE is a custom name you make up (that must conform to a standard). This is the name of your app on the network, it doesn’t have to be the same name as your app, just every device that you want to see each other must use the same one. The service type specifies which protocol and transport layer the application uses. The syntax is “_[protocol]._[transportlayer].” You can name the protocol anything you want but leave the transportlayer the way it is.
Note: If you plan on publishing an app to the app store that uses NSD you should register your protocol to the International Assigned Numbers Authority (IANA). They manage a centralized, authoritative list of service types used by service discovery protocols such as NSD and Bonjour. If you intend to use a new service type, you should reserve it by filling out the IANA Ports and Service registration form.
REQUEST_CONNECT_CLIENT is a constant we use to tell the server what we want to do. At first this will be the only option, but later we will add a display message option.
Now with our tools laid out let’s walk through the logic.
We display our screen with setContentView(R.layout.main);
We create an instance of NsdManager called mNsdManager to use to discover and connect to our server.
We then use NsdManager to discover our service (and server) on the network using our SERVICE_TYPE, we define what type of protocol we are using (for NSD you use NsdManager.PROTOCOL_DNS_SD) and where to go after the service is successfully registered (a interface callback we create called mDiscoveryListener).
Next we define, initialize and implement the DiscoveryListener interface call back. We use this to get all the devices that have registered the service. Since it is an interface we have methods() we must implement (@Override) to receive the NsdServiceInfo for all the servers we discovered.
The methods are:
public void onDiscoveryStarted(String regType) {}
public void onServiceFound(NsdServiceInfo service) {}
public void onServiceLost(NsdServiceInfo service) {}
public void onDiscoveryStopped(String serviceType) {}
public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
public void onStopDiscoveryFailed(String serviceType, int errorCode) {}
The second one is the only one we will really use, the rest are used for logging purposes.
@Override public void onServiceFound(NsdServiceInfo service) { // A service was found! Do something with it. Log.d(TAG, "Service discovery success : " + service); Log.d(TAG, "Host = "+ service.getServiceName()); Log.d(TAG, "port = " + String.valueOf(service.getPort())); if (!service.getServiceType().equals(SERVICE_TYPE)) { // Service type is the string containing the protocol and // transport layer for this service. Log.d(TAG, "Unknown Service Type: " + service.getServiceType()); } else if (service.getServiceName().equals(SERVICE_NAME)) { // The name of the service tells the user what they'd be // connecting to. It could be "Bob's Chat App". Log.d(TAG, "Same machine: " + SERVICE_NAME); } else { Log.d(TAG, "Diff Machine : " + service.getServiceName()); // connect to the service and obtain serviceInfo mNsdManager.resolveService(service, mResolveListener); } }
When any service is found (even if it is not ours we show all the NsdServiceInfo that we discovered and then we point out the service name (which is “Server Device” for what we are looking for) and port number for the service, even if it is not ours (if you have printers on the network or webcams you will see those here too).
Now we start parsing the NsdServiceInfo to find what we want. First we check the SERVICE_TYPE if it is NOT (notice the ! at the beginning of the if statement) _letstalk._tcp. then we log it and keep discovering.
If the SERVICE_TYPE IS _letstalk._tcp. we check the SERVICE_NAME. If the SERVICE_NAME is the same as ours, Client Device, we know we have discovered ourself, so we keep looking.
If the SERVICE_NAME is different than we have found another device that we can connect to. In our case it should be the server. This is where you would create a List if you wanted to make a list of several devices you could connect to, but for simplicity sake if we find our server we immediately resolve the service so we can connect.
To resolve the service we use our instance of NsdManager to start the resolveService() method which takes two parameters the first is the NsdServiceInfo of the device we are wanting to resolve(get IPAddress for), and the second parameter is where to send that information when we get it, which is the ResolveListener interface call back.
Now we define, initialize and implement the ResolveListener interface call back. It has two methods we must implement.
public void onServiceResolved(NsdServiceInfo serviceInfo) {}
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {}
onServiceResolved(NsdServiceInfo serviceInfo) is the one we care about, it gives us the NsdServiceInfo that includes the server IP address this time.
First we double check to make sure we didn’t get our own device’s address by checking the SERVICE_NAME, if we did we return, which goes back to resolving the next device we found.
If we did get the information for our server, we set our local variables that we will use to connect to the server, hostPort is the server port number and hostAddress is the servers IP address.
1
2
3
|
// Obtain port and IP hostPort = serviceInfo.getPort(); hostAddress = serviceInfo.getHost(); |
We then run a custom method(), connectToHost();
private void connectToHost() { if (hostAddress == null) { Log.e(TAG, "Host Address is null"); return; } String ipAddress = getLocalIpAddress(); JSONObject jsonData = new JSONObject(); try { jsonData.put("request", REQUEST_CONNECT_CLIENT); jsonData.put("ipAddress", ipAddress); } catch (JSONException e) { e.printStackTrace(); Log.e(TAG, "can't put request"); return; } new SocketServerTask().execute(jsonData); }
We double check to make sure we have a IP address to connect to, if we don’t we jump out of the connectToHost() method so we don’t try to connect and crash.
If we do have a IP Address we run another custom method(), getLocalIpAddress(); Let’s jump there and then come back.
1
2
3
4
5
|
private String getLocalIpAddress() { WifiManager wm = (WifiManager) getSystemService(WIFI_SERVICE); String ip = Formatter.formatIpAddress(wm.getConnectionInfo().getIpAddress()); return ip; } |
First of all when you get any kind of Wifi information you are going to need request permissions from the user which means we will need a permission in the Manifest file.
1
|
|
Then in the getLocalIpAddress() method we initialize our WifiManager which we will use to get our local IP address, wm.getConnectionInfo().getIpAddress().
Then we take that IP address that is not in a format we can use and run it through the Formatter class which contains a method() that turns the IP Address we retrieved into a String that we can use, Formatter.formatIpAddress().
And we send it back to the connectToHost() method and save it as ipAddress.
Now we are back in the connectToHost() method, we create a JSONObject that we can use to send data to the server.
There are lots of things we could do wrong, so we surround our actions with a try and catch and then start to put data in our JSONObject (jsonData). We use (key, value) pairs to do this so the command ends up looking like this:
1
2
|
jsonData.put( "request" , REQUEST_CONNECT_CLIENT); jsonData.put( "ipAddress" , ipAddress); |
The request “key” contains our REQUEST_CONNECT_CLIENT “value” which is request-connect-client. This will tell the server that we want to connect.
We also send a ipAddress “key” that contains our ipAddress “value” which is the IP address of our device.
We catch our JSONObject errors and print out our stack trace (e.printStackTrace();) if there were any, and jump out of our try with return;
But for fun’s sake, let’s say it worked and then we create a new custom Thread called SocketServerTask() and run it with our jsonData (.execute(jsonData);).
1
|
new SocketServerTask().execute(jsonData); |
makes this:
private class SocketServerTask extends AsyncTask<JSONObject, Void, Void> { private JSONObject jsonData; private boolean success; @Override protected Void doInBackground(JSONObject... params) { Socket socket = null; DataInputStream dataInputStream = null; DataOutputStream dataOutputStream = null; jsonData = params[0]; try { // Create a new Socket instance and connect to host socket = new Socket(hostAddress, SocketServerPort); dataOutputStream = new DataOutputStream( socket.getOutputStream()); dataInputStream = new DataInputStream(socket.getInputStream()); // transfer JSONObject as String to the server dataOutputStream.writeUTF(jsonData.toString()); Log.i(TAG, "waiting for response from host"); // Thread will wait till server replies String response = dataInputStream.readUTF(); if (response != null && response.equals("Connection Accepted")) { success = true; } else { success = false; } } catch (IOException e) { e.printStackTrace(); success = false; } finally { // close socket if (socket != null) { try { Log.i(TAG, "closing the socket"); socket.close(); } catch (IOException e) { e.printStackTrace(); } } // close input stream if (dataInputStream != null) { try { dataInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } // close output stream if (dataOutputStream != null) { try { dataOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } @Override protected void onPostExecute(Void result) { if (success) { Toast.makeText(MainActivity.this, "Connection Done", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this, "Unable to connect", Toast.LENGTH_SHORT).show(); } } }
As you noticed probably quite quickly we are using a AsyncTask class to send the data. AsyncTask is not a bare bones Thread it is actually a “helper” class that uses a Thread to do it’s work
Some detailed, but very good documentation from googles AsyncTask web page.
AsyncTask enables proper and easy use of the UI thread. This class allows to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.
AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent package such as Executor, ThreadPoolExecutor and FutureTask.
An asynchronous task is defined by a computation that runs on a background thread and whose result is published on the UI thread. An asynchronous task is defined by 3 generic types, called Params, Progress and Result, and 4 steps, called onPreExecute, doInBackground, onProgressUpdate and onPostExecute.
So in summary, AsyncTask uses a Thread and Handler, so you don’t have to, and posts it’s results to the UI Thread. It is also best for short tasks, like sending data, not listening for connections, like the server does.
private class SocketServerTask extends AsyncTask<JSONObject, Void, Void> { |
The three parameters are used like this:
JSONObject is the type of Object we are passing in.
The first Void is the type of value you want to pass back to calculate the progress bar.
The second Void is the type of value you want to send back to the UI Thread to display.
Since we are using the AsyncTask to only do work, because what we are doing is going to happen so fast, we won’t be returning any values for the progress bar, and we don’t need to update the UI Thread because we will know it’s complete when the song starts playing.
We use the boolean value (true or false) success in multiple methods() in the SocketServerTask class so we define the variable here so we can get to it. We set it in our doInBackground() method and we retrieve it in our onPostExecute() method.
When we run the .execute() method we are actually running:
protected Void doInBackground(JSONObject... params) {} |
protected means that it can not be called from outside the SocketServerTask class.
Void means we will not be passing any values to the onPostExecute() method. And also means we will be returning null from the doInBackground() method.
JSONObject… The … means array, so the doInBackground() method can take several JSONObjects in an array format.
params is the JSONObject (jsonData) we passed in from the connectToHost() method in the .execute(jsonData); command.
Now we do the same think like we did in our Server Thread:
We initialize our Socket, used to communicate with the server, and set it’s value to null. You may wonder why we initialize it here instead of farther down when we give it an actual value. When we are done using the socket we are going to try to close it from the finally {} section and if we don’t initialize it here we won’t be able to “see” it.
We initialize our DataInputStream, that receives incoming data, and set it to null.
We initialize our DataOutputStream, used to send data to clients, and set it to null.
We create a JSONObject jsonData and set it to the value in index 0 (the first value), which was passed in the params array of JSONObjects.
protected Void doInBackground(JSONObject... params) {} |
Now we are about to do some pretty complicated stuff, Android makes it pretty easy, but if everything is not set up just right, it could easily fail, so we use a try and catch.
We create a new socket to talk to the server using the server IP Address we acquired and a port that we manually agreed upon.
Once we have the socket we can use it to create a DataInputStream and a DataOutputStream.
Now we can send data to the server.
dataOutputStream.writeUTF(jsonData.toString()); |
We take our dataOutputStream and write to the socket in a UTF-8 format which is a way to format text similar to the .xml files used for our layout objects, you will notice in the main.xml file at the beginning:
We convert our jsonData Object to a String, not exactly sure what it would look like, but it will contain two key/value pairs something like request request-connect-client ipAddress 192.168.1.98
then we run
String response = dataInputStream.readUTF(); |
Which blocks the Thread (which would cause our UI Thread to crash if we were running it there) and we wait until the server responds. Once we have a response, since we know it is only returning one string, not a JSONObject or any other object we can just compare the response to a text string.
if (response != null && response.equals(“Connection Accepted”)) {
We could have used a constant like:
final String CONNECTION_VALUE = “Connection Accepted” and then we could check if:
if (response != null && response.equals(CONNECTION_VALUE)) {
but that is a little more than we need at this point.
If the response was “Connection Accepted” then everything was successful and we can set the boolean value success to true, if the response didn’t equal “Connection Accepted” then we set the boolean value success to false.
Then we have our catch statement if something went wrong while performing I/O Input/Output and we can print our stackTrace if needed.
Finally which means after the Thread has performed it’s task we can try to close our socket, DataInputStream and our DataOutputStream. If we run into issues we catch the IOExceptions and print out our stackTrace for debugging.
After all that we have to return something because doInBackground() must have a return value, we are able to set it to Void like we did in this instance as a work around, but we still have to return something so we return null; which doesn’t go anywhere. Since we are returning Void the parameter we take in on the onPostExecute() method needs to be Void so it doesn’t expect a real value.
1
2
3
4
5
6
7
8
|
@Override protected void onPostExecute(Void result) { if (success) { Toast.makeText(MainActivity. this , "Connection Done" , Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity. this , "Unable to connect" , Toast.LENGTH_SHORT).show(); } } |
Then we check our success variable and Toast a message to the screen accordingly.
For a better example of the onPostExecute() method let’s say that we are returning a Bitmap from the doInBackground() method.
This:
private class SocketServerTask extends AsyncTask<JSONObject, Void, Void> { |
Would become this:
private class SocketServerTask extends AsyncTask<JSONObject, Void, Bitmap> { |
This:
protected Void doInBackground(JSONObject... params) { |
Would become this:
protected Bitmap doInBackground(JSONObject... params) { |
This:
return null ; |
Would become this:
return bitmap; //bitmap being a Bitmap Object |
This:
protected void onPostExecute(Void result) { |
Would become this:
protected void onPostExecute(Bitmap result) { |
To finish out this project we define the onPause(), onResume(), onDestroy() methods. These are all protected so you can only run them from this class. They all make sure mNsdManager has been created because if you try to stop a service when it doesn’t exist the program will crash.
onPause() will stop service discovery because if the app is not running there is no need to waste resources discovering things we won’t use (memory leak). Then the default onPause() method will run.
onResume() will continue discovering services, and reconnect to our server in this case, on resuming, after the default onResume() method runs.
onDestroy() will stop service discovery because if the app is not running there is no need to waste resources discovering things we won’t use (memory leak). Then the default onDestroy() method will run.
Here is the main.xml file
android:layout_width= "fill_parent" android:layout_height= "fill_parent" android:gravity= "center" android:orientation= "vertical" > |
Now you can compile and run the server on one device and the client on the other, make sure both devices are on the same network. When you run the client you should see Toasts appear on both devices.
For my next post I will add the ability to send text from the client to the server to this project.