Posted By: Fabio Chiarani
Why? and How?
Have you ever been asking yourself, once you read the official Android bluetooth guide, how to manage it inside your app? How do you keep the connection active even when you move between different activities and fragments?
Well, in this guide, I’ll try to show you how I implemented the bluetooth communication through a Service
in order to manage the bluetooth and the connection with the various activities exploiting the Service Binding,
and implementing an callback listener for the activities that want to receive information about the status of the bluetooth communication.
In this guide we are going to create four files:
BluetoothSDKService
: which implements the bluetooth functionalities and emit theLocalBroadcast
messages during the operationsBluetoothSDKListenerHelper
: which implements theBroadcastReceiver
and trigger theIBluetoothSDKListener
functionsIBluetoothSDKListener
: ourInterface
that defines the callback functionsBluetoothUtils
: which contains the action names defined to filter the events inside in theBroadcastReceiver
1) Define actions
The first step is to define the BluetoothUtils.kt
file, which contains the actions we want to be notified inside our activity:
s magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc,
class BluetoothUtils { | |
companion object { | |
val ACTION_DISCOVERY_STARTED = "ACTION_DISCOVERY_STARTED" | |
val ACTION_DISCOVERY_STOPPED = "ACTION_DISCOVERY_STOPPED" | |
val ACTION_DEVICE_FOUND = "ACTION_DEVICE_FOUND" | |
val ACTION_DEVICE_CONNECTED = "ACTION_DEVICE_CONNECTED" | |
val ACTION_DEVICE_DISCONNECTED = "ACTION_DEVICE_DISCONNECTED" | |
val ACTION_MESSAGE_RECEIVED = "ACTION_MESSAGE_RECEIVED" | |
val ACTION_MESSAGE_SENT = "ACTION_MESSAGE_SENT" | |
val ACTION_CONNECTION_ERROR = "ACTION_CONNECTION_ERROR" | |
val EXTRA_DEVICE = "EXTRA_DEVICE" | |
val EXTRA_MESSAGE = "EXTRA_MESSAGE" | |
} | |
} |
I have defined a few, but you can add them as you like.
2) Define events-callback functions
The second step is to define our interface, which will contain the events that correspond to the actions we defined in step one. So, let’s go ahead and define the IBluetoothSDKListener
as:
interface IBluetoothSDKListener { | |
/** | |
* from action BluetoothUtils.ACTION_DISCOVERY_STARTED | |
*/ | |
fun onDiscoveryStarted() | |
/** | |
* from action BluetoothUtils.ACTION_DISCOVERY_STOPPED | |
*/ | |
fun onDiscoveryStopped() | |
/** | |
* from action BluetoothUtils.ACTION_DEVICE_FOUND | |
*/ | |
fun onDeviceDiscovered(device: BluetoothDevice?) | |
/** | |
* from action BluetoothUtils.ACTION_DEVICE_CONNECTED | |
*/ | |
fun onDeviceConnected(device: BluetoothDevice?) | |
/** | |
* from action BluetoothUtils.ACTION_MESSAGE_RECEIVED | |
*/ | |
fun onMessageReceived(device: BluetoothDevice?, message: String?) | |
/** | |
* from action BluetoothUtils.ACTION_MESSAGE_SENT | |
*/ | |
fun onMessageSent(device: BluetoothDevice?) | |
/** | |
* from action BluetoothUtils.ACTION_CONNECTION_ERROR | |
*/ | |
fun onError(message: String?) | |
/** | |
* from action BluetoothUtils.ACTION_DEVICE_DISCONNECTED | |
*/ | |
fun onDeviceDisconnected() | |
} |
This interface will be later implemented inside our activity, or fragment, that wants to perform some actions when it receives an event. For example, when the device connects, the function onDeviceDiscovered
is triggered, and then you can go to do certain operations, such as, as we will see in the next steps, send a message via bluetooth to the device just connected through our BluetoothSDKService
.
3) Define BroadcastReceiver
The next step is to define our BroadcastReceiver
, which will have the task of filtering the intent with our actions defined before received by the LocalBroadcastManager
, to trigger the callback functions defined in the previous section. So we define BluetoothSDKListenerHelper
as:
class BluetoothSDKListenerHelper { | |
companion object { | |
private var mBluetoothSDKBroadcastReceiver: BluetoothSDKBroadcastReceiver? = null | |
class BluetoothSDKBroadcastReceiver : BroadcastReceiver() { | |
private var mGlobalListener: IBluetoothSDKListener? = null | |
public fun setBluetoothSDKListener(listener: IBluetoothSDKListener) { | |
mGlobalListener = listener | |
} | |
public fun removeBluetoothSDKListener(listener: IBluetoothSDKListener): Boolean { | |
if (mGlobalListener == listener) { | |
mGlobalListener = null | |
} | |
return mGlobalListener == null | |
} | |
override fun onReceive(context: Context?, intent: Intent?) { | |
val device = | |
intent!!.getParcelableExtra<BluetoothDevice>(BluetoothUtils.EXTRA_DEVICE) | |
val message = intent.getStringExtra(BluetoothUtils.EXTRA_MESSAGE) | |
when (intent.action) { | |
BluetoothUtils.ACTION_DEVICE_FOUND -> { | |
mGlobalListener!!.onDeviceDiscovered(device) | |
} | |
BluetoothUtils.ACTION_DISCOVERY_STARTED -> { | |
mGlobalListener!!.onDiscoveryStarted() | |
} | |
BluetoothUtils.ACTION_DISCOVERY_STOPPED -> { | |
mGlobalListener!!.onDiscoveryStopped() | |
} | |
BluetoothUtils.ACTION_DEVICE_CONNECTED -> { | |
mGlobalListener!!.onDeviceConnected(device) | |
} | |
BluetoothUtils.ACTION_MESSAGE_RECEIVED -> { | |
mGlobalListener!!.onMessageReceived(device, message) | |
} | |
BluetoothUtils.ACTION_MESSAGE_SENT -> { | |
mGlobalListener!!.onMessageSent(device) | |
} | |
BluetoothUtils.ACTION_CONNECTION_ERROR -> { | |
mGlobalListener!!.onError(message) | |
} | |
BluetoothUtils.ACTION_DEVICE_DISCONNECTED -> { | |
mGlobalListener!!.onDeviceDisconnected() | |
} | |
} | |
} | |
} | |
public fun registerBluetoothSDKListener( | |
context: Context?, | |
listener: IBluetoothSDKListener | |
) { | |
if (mBluetoothSDKBroadcastReceiver == null) { | |
mBluetoothSDKBroadcastReceiver = BluetoothSDKBroadcastReceiver() | |
val intentFilter = IntentFilter().also { | |
it.addAction(BluetoothUtils.ACTION_DEVICE_FOUND) | |
it.addAction(BluetoothUtils.ACTION_DISCOVERY_STARTED) | |
it.addAction(BluetoothUtils.ACTION_DISCOVERY_STOPPED) | |
it.addAction(BluetoothUtils.ACTION_DEVICE_CONNECTED) | |
it.addAction(BluetoothUtils.ACTION_MESSAGE_RECEIVED) | |
it.addAction(BluetoothUtils.ACTION_MESSAGE_SENT) | |
it.addAction(BluetoothUtils.ACTION_CONNECTION_ERROR) | |
it.addAction(BluetoothUtils.ACTION_DEVICE_DISCONNECTED) | |
} | |
LocalBroadcastManager.getInstance(context!!).registerReceiver( | |
mBluetoothSDKBroadcastReceiver!!, intentFilter | |
) | |
} | |
mBluetoothSDKBroadcastReceiver!!.setBluetoothSDKListener(listener) | |
} | |
public fun unregisterBluetoothSDKListener( | |
context: Context?, | |
listener: IBluetoothSDKListener | |
) { | |
if (mBluetoothSDKBroadcastReceiver != null) { | |
val empty = mBluetoothSDKBroadcastReceiver!!.removeBluetoothSDKListener(listener) | |
if (empty) { | |
LocalBroadcastManager.getInstance(context!!) | |
.unregisterReceiver(mBluetoothSDKBroadcastReceiver!!) | |
mBluetoothSDKBroadcastReceiver = null | |
} | |
} | |
} | |
} | |
} |
In the activity, or fragment we will implement our IBluetoothSDKListener
, which we will register through the two functions registerBluetoothSDKListner()
and unregisterBluetoothSDKListner()
. For example:
class CoolFragment() : BottomSheetDialogFragment() { | |
private lateinit var mService: BluetoothSDKService | |
private lateinit var binding: FragmentPopupDiscoveredLabelerDeviceBinding | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
} | |
override fun onCreateView( | |
inflater: LayoutInflater, | |
container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View? { | |
val view = inflater.inflate(R.layout.fragment_popup_discovered_labeler_device, container,false) | |
binding = FragmentPopupDiscoveredLabelerDeviceBinding.bind(view) | |
bindBluetoothService() | |
// Register Listener | |
BluetoothSDKListenerHelper.registerBluetoothSDKListener(requireContext(), mBluetoothListener) | |
return view | |
} | |
/** | |
* Bind Bluetooth Service | |
*/ | |
private fun bindBluetoothService() { | |
// Bind to LocalService | |
Intent( | |
requireActivity().applicationContext, | |
BluetoothSDKService::class.java | |
).also { intent -> | |
requireActivity().applicationContext.bindService( | |
intent, | |
connection, | |
Context.BIND_AUTO_CREATE | |
) | |
} | |
} | |
/** | |
* Handle service connection | |
*/ | |
private val connection = object : ServiceConnection { | |
override fun onServiceConnected(className: ComponentName, service: IBinder) { | |
val binder = service as BluetoothSDKService.LocalBinder | |
mService = binder.getService() | |
} | |
override fun onServiceDisconnected(arg0: ComponentName) { | |
} | |
} | |
private val mBluetoothListener: IBluetoothSDKListener = object : IBluetoothSDKListener { | |
override fun onDiscoveryStarted() { | |
} | |
override fun onDiscoveryStopped() { | |
} | |
override fun onDeviceDiscovered(device: BluetoothDevice?) { | |
} | |
override fun onDeviceConnected(device: BluetoothDevice?) { | |
// Do stuff when is connected | |
} | |
override fun onMessageReceived(device: BluetoothDevice?, message: String?) { | |
} | |
override fun onMessageSent(device: BluetoothDevice?) { | |
} | |
override fun onError(message: String?) { | |
} | |
} | |
override fun onDestroy() { | |
super.onDestroy() | |
// Unregister Listener | |
BluetoothSDKListenerHelper.unregisterBluetoothSDKListener(requireContext(), mBluetoothListener) | |
} | |
} |
Now our fragment can be triggered for events received by the BroadcastListener
, which relays them through callbacks to our fragment interface. What’s missing now? Well, the important piece: the Bluetooth Service!
4) Define Bluetooth Service
And now, the clue part, the Bluetooth Service. We are going to define a class that extends the Service
, where we define within it the functions to allow the Service binding and manage the Bluetooth connection threads:
class BluetoothSDKService : Service() { | |
// Service Binder | |
private val binder = LocalBinder() | |
// Bluetooth stuff | |
private lateinit var bluetoothAdapter: BluetoothAdapter | |
private lateinit var pairedDevices: MutableSet<BluetoothDevice> | |
private var connectedDevice: BluetoothDevice? = null | |
private val MY_UUID = "..." | |
private val RESULT_INTENT = 15 | |
// Bluetooth connections | |
private var connectThread: ConnectThread? = null | |
private var connectedThread: ConnectedThread? = null | |
private var mAcceptThread: AcceptThread? = null | |
// Invoked only first time | |
override fun onCreate() { | |
super.onCreate() | |
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() | |
} | |
// Invoked every service star | |
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | |
return START_STICKY | |
} | |
/** | |
* Class used for the client Binder. | |
*/ | |
inner class LocalBinder : Binder() { | |
/* | |
Function that can be called from Activity or Fragment | |
*/ | |
} | |
/** | |
* Broadcast Receiver for catching ACTION_FOUND aka new device discovered | |
*/ | |
private val discoveryBroadcastReceiver = object : BroadcastReceiver() { | |
override fun onReceive(context: Context, intent: Intent) { | |
/* | |
Our broadcast receiver for manage Bluetooth actions | |
*/ | |
} | |
} | |
private inner class AcceptThread : Thread() { | |
// Body | |
} | |
private inner class ConnectThread(device: BluetoothDevice) : Thread() { | |
// Body | |
} | |
@Synchronized | |
private fun startConnectedThread( | |
bluetoothSocket: BluetoothSocket?, | |
) { | |
connectedThread = ConnectedThread(bluetoothSocket!!) | |
connectedThread!!.start() | |
} | |
private inner class ConnectedThread(private val mmSocket: BluetoothSocket) : Thread() { | |
// Body | |
} | |
override fun onDestroy() { | |
super.onDestroy() | |
try { | |
unregisterReceiver(discoveryBroadcastReceiver) | |
} catch (e: Exception) { | |
// already unregistered | |
} | |
} | |
override fun onBind(intent: Intent?): IBinder? { | |
return binder | |
} | |
private fun pushBroadcastMessage(action: String, device: BluetoothDevice?, message: String?) { | |
val intent = Intent(action) | |
if (device != null) { | |
intent.putExtra(BluetoothUtils.EXTRA_DEVICE, device) | |
} | |
if (message != null) { | |
intent.putExtra(BluetoothUtils.EXTRA_MESSAGE, message) | |
} | |
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent) | |
} | |
} |
Job Offers
To make the gist readable, I commented out the parts about the threads, which are the same you can get from the official doc.
As you can see, in the body of the LocalBinder
you can define the functions that will be visible to the activity after the binding with it. For example, we can define functions for discovery, send message or connection operations, which will then perform operations internally to the service.
/** | |
* Class used for the client Binder. | |
*/ | |
inner class LocalBinder : Binder() { | |
/** | |
* Enable the discovery, registering a broadcastreceiver {@link discoveryBroadcastReceiver} | |
* The discovery filter by LABELER_SERVER_TOKEN_NAME | |
*/ | |
public fun startDiscovery(context: Context) { | |
val filter = IntentFilter(BluetoothDevice.ACTION_FOUND) | |
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED) | |
registerReceiver(discoveryBroadcastReceiver, filter) | |
bluetoothAdapter.startDiscovery() | |
pushBroadcastMessage(BluetoothUtils.ACTION_DISCOVERY_STARTED, null, null) | |
} | |
/** | |
* stop discovery | |
*/ | |
public fun stopDiscovery() { | |
bluetoothAdapter.cancelDiscovery() | |
pushBroadcastMessage(BluetoothUtils.ACTION_DISCOVERY_STOPPED, null, null) | |
} | |
// other stuff | |
} |
Then, within the threads that manage the sockets, you can use the pushBroadcastMessage()
function to generate events and add a payload such as the remote device and a message. For example:
private inner class ConnectedThread(private val mmSocket: BluetoothSocket) : Thread() { | |
private val mmInStream: InputStream = mmSocket.inputStream | |
private val mmOutStream: OutputStream = mmSocket.outputStream | |
private val mmBuffer: ByteArray = ByteArray(1024) // mmBuffer store for the stream | |
override fun run() { | |
var numBytes: Int // bytes returned from read() | |
// Keep listening to the InputStream until an exception occurs. | |
while (true) { | |
// Read from the InputStream. | |
numBytes = try { | |
mmInStream.read(mmBuffer) | |
} catch (e: IOException) { | |
pushBroadcastMessage( | |
BluetoothUtils.ACTION_CONNECTION_ERROR, | |
null, | |
"Input stream was disconnected" | |
) | |
break | |
} | |
val message = String(mmBuffer, 0, numBytes) | |
// Send to broadcast the message | |
pushBroadcastMessage( | |
BluetoothUtils.ACTION_MESSAGE_RECEIVED, | |
mmSocket.remoteDevice, | |
message | |
) | |
} | |
} | |
// Call this from the main activity to send data to the remote device. | |
fun write(bytes: ByteArray) { | |
try { | |
mmOutStream.write(bytes) | |
// Send to broadcast the message | |
pushBroadcastMessage( | |
BluetoothUtils.ACTION_MESSAGE_SENT, | |
mmSocket.remoteDevice, | |
null | |
) | |
} catch (e: IOException) { | |
pushBroadcastMessage( | |
BluetoothUtils.ACTION_CONNECTION_ERROR, | |
null, | |
"Error occurred when sending data" | |
) | |
return | |
} | |
} | |
// Call this method from the main activity to shut down the connection. | |
fun cancel() { | |
try { | |
mmSocket.close() | |
} catch (e: IOException) { | |
pushBroadcastMessage( | |
BluetoothUtils.ACTION_CONNECTION_ERROR, | |
null, | |
"Could not close the connect socket" | |
) | |
} | |
} | |
} |
Conclusion
We have seen how from our activity we can bind the Bluetooth service (1)
, which performs and manages Bluetooth operations. Within it, we can issue broadcast events (2)
which are received by the Bluetooth listener. Once received, the Bluetooth listener calls in turn the function of the interface implemented (4)
in our Activity, registered to the bluetooth Listener (3)
My advice is to always follow the official guidelines and clean code writing guidelines.