Developing Bluetooth-enabled apps with Java

A good starting explanation about network programming in general comes from this page. Look at the whole book – it’s called “An Introduction to Bluetooth Programming” and I find it a very good read.

Emulation

Before we start, I’d like to mention that, for development, it’s much easier to develop using emulation of bluetooth / mobile phone. For that, I’ve used BlueCove and MicroEmulator, respectively. The glue between them is the BlueCove JSR-82 Emulator module.

Scenario

The situation we’ll examine is simple. We have two devices – in this case a PC and a phone – communicating with each other. The example would be a simple searcher for Stack Exchange sites. The user on the mobile phone would type a word, that gets sent to the server on the PC, it searches using api.stackexchange.com and returns the results to the phone, which displays them.

Following are the steps needed for the above to happen.

Discovery

To communicate, a bluetooth app uses the SDP (Service Discovery Protocol) to find the other devices and services they provide. In this case, as PC acts as the server, the phone would initiate the discovery, i.e. be a client. The basic idea is presented in BlueCove’s javadoc. I’ve made a helper class called BluetoothUtils:

package com.icyrock.bluetooth;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.bluetooth.DataElement;
import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class BluetoothUtils {
  private static final int btAttrServiceName = 0x0100;
  private final static Logger log = LoggerFactory.getLogger(BluetoothUtils.class);

  public List<RemoteDevice> discoverDevices() {
    final Object sync = new Object();
    final List<RemoteDevice> devs = new ArrayList<RemoteDevice>();

    DiscoveryListener listener = new DiscoveryListener() {
      public void deviceDiscovered(RemoteDevice dev, DeviceClass devClass) {
        try {
          log.info("Found device: address: {}, name: {}",
            dev.getBluetoothAddress(),
            dev.getFriendlyName(false));
          devs.add(dev);
        } catch (IOException e) {
          log.error(e.toString(), e);
        }
      }

      public void inquiryCompleted(int discType) {
        log.info("Device inquiry completed");
        synchronized (sync) {
          sync.notifyAll();
        }
      }

      public void serviceSearchCompleted(int transId, int respCode) {
      }

      public void servicesDiscovered(int transId, ServiceRecord[] servRecord) {
      }
    };

    synchronized (sync) {
      boolean started;
      try {
        started = LocalDevice.getLocalDevice().getDiscoveryAgent()
          .startInquiry(DiscoveryAgent.GIAC, listener);
        if (started) {
          log.info("Device inquiry started");
          sync.wait();
          log.info("Devices count: {}", devs.size());
        }
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }

    return devs;
  }

  public List<String> searchForServices(List<RemoteDevice> devices, String uuidStr) {
    final Object sync = new Object();
    final List<String> urls = new ArrayList<String>();

    DiscoveryListener listener = new DiscoveryListener() {
      public void deviceDiscovered(RemoteDevice dev, DeviceClass devClass) {
      }

      public void inquiryCompleted(int discType) {
      }

      public void servicesDiscovered(int transId, ServiceRecord[] servRecord) {
        for (int i = 0; i < servRecord.length; i++) {
          String url = servRecord[i].getConnectionURL(ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
          if (url != null) {
            urls.add(url);
            DataElement name = servRecord[i].getAttributeValue(btAttrServiceName);
            log.info("Service found: url: {}, name: {}", url, name);
          }
        }
      }

      public void serviceSearchCompleted(int transId, int respCode) {
        log.info("Service search completed");
        synchronized (sync) {
          sync.notifyAll();
        }
      }

    };

    UUID[] uuidArr = new UUID[] { new UUID(uuidStr, false) };
    int[] attrIds = new int[] { btAttrServiceName };

    for (RemoteDevice device : devices) {
      synchronized (sync) {
        try {
          log.info("Searching for service: {} on device: {} / {}",
            new Object[] { uuidStr, device.getBluetoothAddress(), device.getFriendlyName(false) });
          LocalDevice.getLocalDevice().getDiscoveryAgent()
            .searchServices(attrIds, uuidArr, device, listener);
          sync.wait();
        } catch (Exception e) {
          log.error(e.toString(), e);
        }
      }
    }

    return urls;
  }
}

This one has two methods:

  • public List<RemoteDevice> discoverDevices() – Searches for the devices and returns a list of devices that have been found
  • public List<String> searchForServices(List<RemoteDevice> devices, String uuidStr) – Searches for the service given by uuidStr on the devices in the devices list. It returns the URLs that can be used to access these services

These methods are going to be used in client and server later.

Client

The client is fairly simple:

package com.icyrock.bluetooth;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

import javax.bluetooth.RemoteDevice;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.google.common.collect.Lists;
import com.icyrock.spring.SpringUtils;

@Component
public class StackExchangeBluetoothRfcommClient {
  private final static Logger log = LoggerFactory.getLogger(StackExchangeBluetoothRfcommClient.class);
  @Autowired
  private BluetoothUtils bluetoothUtils;
  private final static String uuid = "88e08d4875344ff88f18ec4cc70ee702";

  public void run() {
    try {
      startClient();
    } catch (Throwable t) {
      log.error(t.toString(), t);
    }
  }

  private void startClient() throws Exception {
    List<RemoteDevice> devices = bluetoothUtils.discoverDevices();
    List<String> urls = bluetoothUtils.searchForServices(devices, uuid);

    if (urls.size() > 0) {
      String keywords = "bluetooth";

      StreamConnection streamConn = (StreamConnection) Connector.open(urls.get(0));

      OutputStream os = streamConn.openOutputStream();
      IOUtils.writeLines(Lists.newArrayList(keywords), null, os);
      os.close();

      InputStream is = streamConn.openInputStream();
      String result = IOUtils.toString(is);
      is.close();

      log.info("Result: {}", result);

      streamConn.close();

      log.info("Client done");
    }
  }

  public static void main(String[] args) {
    System.setProperty("bluecove.stack", "emulator");
    StackExchangeBluetoothRfcommClient sebc = SpringUtils.getDefaultContext()
      .getBean(StackExchangeBluetoothRfcommClient.class);
    sebc.run();
  }
}

Aside from the boilerplate code needed, there are two important notes above. First, in main method, there’s this line:

    System.setProperty("bluecove.stack", "emulator");

This one is used when testing this with BlueCove Bluetooth emulation, to signal BlueCove to use the emulator instead of the real bluetooth stack. Read more about this here.

Second, note this line:

  private final static String uuid = "88e08d4875344ff88f18ec4cc70ee702";

This one defines the UUID of the service that we are going to look for. In Bluetooth, all services are represented by UUID. To generate these in Linux, you can do something like:

$ uuidgen|tr -d '-'
db56eca4204d4f478063dacab16e805f

The other things are all in startClient method. The method does this:

  • Searches for all devices
  • Searches for the given service (identified by the uuid) on all found devices
  • If the search result yields any URLs, pick the first one (the other option would be to present the user with the choice of which URL to use)
  • Open a StreamConnection – this is basically a RFCOMM channel
  • Write the search keywords to the output stream
  • Read the response from the server and log it

Note that I’m using a few helper libraries here:

Here’s a Gradle config for this project:

apply plugin: 'java'

repositories {
  mavenCentral()
}

dependencies {
  compile 'net.sf.bluecove:bluecove:2.1.0'
  runtime 'net.sf.bluecove:bluecove-gpl:2.1.0'
  runtime 'net.sf.bluecove:bluecove-emu:2.1.0'
  
  compile 'ch.qos.logback:logback-classic:1.0.7'
  compile 'commons-io:commons-io:2.4'
  compile 'org.apache.httpcomponents:httpclient:4.2.1'
  compile 'org.apache.httpcomponents:fluent-hc:4.2.1'
  compile 'com.fasterxml.jackson.core:jackson-databind:2.1.0'
 
  compile 'org.springframework:spring-context:3.1.2.RELEASE'
  runtime 'asm:asm:3.3.1'
  runtime 'cglib:cglib-nodep:2.2.2'
  runtime 'org.slf4j:jcl-over-slf4j:1.6.6'
  
  compile 'com.google.guava:guava:13.0.1'
}

StreamUtils

One problem that I encountered is that you cannot use BufferedReader to read from InputStream provided by the bluetooth connection. The issue is that readLine will block until the whole line is read, but the way it works is it reads chunks of data to be efficient and thus doesn’t know when the line ends. In order to circumvent this, I’m using a simple char-by-char reading, which is hacky, inefficient and might not work properly sometimes, but solves this problem and works well for this example.

package com.icyrock.bluetooth;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.google.common.base.Joiner;

@Component
public class StreamUtils {
  private final static Logger log = LoggerFactory.getLogger(StreamUtils.class);

  private String readLine(InputStream inputStream) {
    StringBuilder sb = new StringBuilder();
    int sleeps = 0;
    boolean haveNewLine = false;

    while (true) {
      int byteRead;
      try {
        if (inputStream.available() == 0) {
          if (sleeps < 3) {
            sleeps++;
            Thread.sleep(1000);
            continue;
          } else {
            break;
          }
        }

        byteRead = inputStream.read();
      } catch (Exception e) {
        throw new RuntimeException(e);
      }

      if (byteRead == -1) {
        break;
      }

      char ch = (char) byteRead;
      if (ch == '\n') {
        haveNewLine = true;
        break;
      }
      sb.append(ch);
    }
    return haveNewLine ? sb.toString() : null;
  }

  public List<String> readLines(InputStream inputStream) {
    List<String> lines = new ArrayList<>();
    while (true) {
      String line = readLine(inputStream);
      if (line == null) {
        break;
      }
      lines.add(line);
    }
    return lines;
  }

  public String readLinesAsStr(InputStream inputStream) {
    return Joiner.on("\n").join(readLines(inputStream));
  }
}

Server

Server is pretty simple, too:

package com.icyrock.bluetooth;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;

import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.LocalDevice;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;

import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.utils.URIBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.icyrock.spring.SpringUtils;

@Component
public class StackExchangeBluetoothRfcommServer {
  private final static Logger log = LoggerFactory.getLogger(StackExchangeBluetoothRfcommServer.class);
  private final static String uuid = "88e08d4875344ff88f18ec4cc70ee702";
  @Autowired
  private StreamUtils streamUtils;

  public void run() {
    try {
      startServer();
    } catch (Throwable t) {
      log.error(t.toString(), t);
    }
  }

  private void startServer() throws Exception {
    LocalDevice.getLocalDevice().setDiscoverable(DiscoveryAgent.GIAC);

    StreamConnectionNotifier serverConnection = (StreamConnectionNotifier) Connector.open(
      "btspp://localhost:" + uuid + ";name=StackExchangeBluetoothServer");

    while (true) {
      log.info("Waiting for connection");
      StreamConnection streamConn = serverConnection.acceptAndOpen();

      log.info("Request received");

      InputStream is = streamConn.openInputStream();
      String content = streamUtils.readLinesAsStr(is);
      is.close();

      List<String> resp;
      if (content == null || content.equals("")) {
        log.info("Empty keywords received from the client.");
        resp = Lists.newArrayList("Please provide keywords to search for");
      } else {
        resp = queryStackExchange(content);
      }

      OutputStream os = streamConn.openOutputStream();
      IOUtils.writeLines(resp, null, os);
      os.close();

      streamConn.close();
    }
  }

  @SuppressWarnings("unchecked")
  private List<String> queryStackExchange(String keywords) throws Exception {
    // Uncomment if you want to test without actually making the calls to StackExchange
//    if(true) {
//      return Lists.newArrayList("title1", "title2", "title3");
//    }
    
    URI uri = new URIBuilder()
      .setScheme("https")
      .setHost("api.stackexchange.com")
      .setPath("/2.1/search")
      .setParameter("order", "desc")
      .setParameter("intitle", keywords)
      .setParameter("site", "stackoverflow")
      .setParameter("pagesize", "5")
      .build();

    HttpResponse httpResp = Request.Get(uri)
      .connectTimeout(5000)
      .socketTimeout(5000)
      .execute().returnResponse();

    List<String> ret = new ArrayList<String>();
    if (httpResp.getStatusLine().getStatusCode() == 200) {
      HttpEntity httpEnt = httpResp.getEntity();

      InputStream is = httpEnt.getContent();
      Header contentEncoding = httpResp.getFirstHeader("Content-Encoding");
      if (contentEncoding != null && "gzip".equals(contentEncoding.getValue())) {
        is = new GZIPInputStream(is);
      }
      String response = IOUtils.toString(is);
      is.close();

      ObjectMapper om = new ObjectMapper();
      Map<String, Object> map = om.readValue(response, Map.class);
      List<Map<String, Object>> items = (List<Map<String, Object>>) map.get("items");

      for (Map<String, Object> item : items) {
        ret.add((String) item.get("title"));
      }
    }

    log.info("Result: {}", Joiner.on(", ").join(ret));
    return ret;
  }

  public static void main(String[] args) {
    System.setProperty("bluecove.stack", "emulator");
    StackExchangeBluetoothRfcommServer sebs = SpringUtils.getDefaultContext()
      .getBean(StackExchangeBluetoothRfcommServer.class);
    sebs.run();
  }
}

In startServer method, it listens for the connection. When one is established, it reads the keywords from the client. If nothing was read, it will reply with a help message. If keywords were read, it will query the StackExchange. The query is separated into queryStackExchange method and uses Apache HttpClient fluid interface – seems pretty nice to me. If the response was received, it will un-GZIP it if needed and then use Jakson JSON parser to actually get the titles from the response.