The idea of writing this tutorial came from the discussion on Reddit recently. A young man was struggling to make things work. He was combing through various blogs and tutorials which are mostly written in Java but could not port the code to Kotlin.
I did use the Bluetooth to transfer data for one of my old applications also using Java, so I’ve decided to pull the code from the said app and convert if to Kotlin.
As a result of this exercise, a small demo application was created, that takes user’s input string and sends it to the same application on the other Android device. The receiving side reads the message and appends it to the screen.
The resulting code is far from production quality but it illustrates the flow and techniques that could be used to transfer data between devices.
If you want to test the project code make sure that bluetooth is enabled on both devices and devices are paired.
Workflow
In general, transmitting data over bluetooth is not much different from transmitting them via the other channels:
- Receiving party creates a server that waits for the clients to connect
- Sending party initiates the connection, both sides getting sockets for data transmission
- Parties exchange data
In the world of the Bluetooth it usually implemented as described on the next diagram
Receiving party creates a Server Controller, which is running in its own thread. Server Controller creates
a service listener identified by the name and UUID. The clients then will use the UUID to access a specific service.
Listener creates a Server Socket that placed into waiting state using accept()
method.
The client on the other hand, initiates a connection to the specified device attempting to access the service,
that is identified by UUID. Once connection is established, accept()
method call returns a socket object,
which is used to create a Server thread that used to communicate with the client.
Once exchange is over, both sides should close the sockets.
Implementation
Let’s take a look into the simple implementation of the above flow with Kotlin.
First, let’s create a BluetoothServerController class:
val uuid: UUID = UUID.fromString("8989063a-c9af-463a-b3f1-f21d9b2b827b")class BluetoothServerController(activity: MainActivity) : Thread() {
private var cancelled: Boolean
private val serverSocket: BluetoothServerSocket?
private val activity = activity
init {
val btAdapter = BluetoothAdapter.getDefaultAdapter()
if (btAdapter != null) {
this.serverSocket = btAdapter.listenUsingRfcommWithServiceRecord("test", uuid) // 1
this.cancelled = false
} else {
this.serverSocket = null
this.cancelled = true
}
}
override fun run() {
var socket: BluetoothSocket
while(true) {
if (this.cancelled) {
break
}
try {
socket = serverSocket!!.accept() // 2
} catch(e: IOException) {
break
}
if (!this.cancelled && socket != null) {
Log.i("server", "Connecting")
BluetoothServer(this.activity, socket).start() // 3
}
}
}
fun cancel() {
this.cancelled = true
this.serverSocket!!.close()
}
}
There are three key things in the above code that enable the server (marked with the numbered comments):
- Create a server socket, identified by the
uuid
, in the class constructor - Once thread execution started wait for the client connections using
accept()
method - Once client established connection
accept()
method returns a BluetoothSocket reference that gives access to the input and output streams. We use this socket to start the Server thread.
The BluetoothServer class looks like the following:
class BluetoothServer(private val activity: MainActivity, private val socket: BluetoothSocket): Thread() {
private val inputStream = this.socket.inputStream
private val outputStream = this.socket.outputStream
override fun run() {
try {
val available = inputStream.available()
val bytes = ByteArray(available)
Log.i("server", "Reading")
inputStream.read(bytes, 0, available)
val text = String(bytes)
Log.i("server", "Message received")
Log.i("server", text)
activity.appendText(text)
} catch (e: Exception) {
Log.e("client", "Cannot read data", e)
} finally {
inputStream.close()
outputStream.close()
socket.close()
}
}
}
Inside the class’s run()
method we just read a message from the client and add it to the screen.
The client class will look like the following:
class BluetoothClient(device: BluetoothDevice): Thread() {
private val socket = device.createRfcommSocketToServiceRecord(uuid)
override fun run() {
Log.i("client", "Connecting")
this.socket.connect()
Log.i("client", "Sending")
val outputStream = this.socket.outputStream
val inputStream = this.socket.inputStream
try {
outputStream.write(message.toByteArray())
outputStream.flush()
Log.i("client", "Sent")
} catch(e: Exception) {
Log.e("client", "Cannot send", e)
} finally {
outputStream.close()
inputStream.close()
this.socket.close()
}
}
}
It simply attempts to access selected device’s service, again identified by the same uuid
. Once connected,
the code uses the socket’s output stream to send a message that user typed into the input box.
To enable the server just run the following in the activity’s onCreate()
method:
BluetoothServerController(this).start()
In order to send a message, the application code should give user the list of available devices and once the user selected device where they want to send a message in question the item select event handler makes the following call:
BluetoothClient(device).start()
The variable device
in here holds the object of BluetoothDevice class representing device selected by user.
The complete application source code can be downloaded from GitHub.