Почему? И как?
Вы когда-нибудь задавали себе вопрос, прочитав официальное руководство по bluetooth для Android, как управлять им внутри вашего приложения? Как сохранить соединение активным, даже когда вы переходите от одного действия к другому?
Что ж, в этом руководстве я постараюсь показать вам, как я реализовал связь bluetooth через Service, чтобы управлять bluetooth и соединением с различными действиями, используя Service Binding, а также установил слушатель обратного вызова для операций, получающих информацию о состоянии связи bluetooth.
В этом руководстве мы создадим четыре файла:
BluetoothSDKService
:который реализует функциональные возможности bluetooth и выдаетLocalBroadcast
сообщения во время операцийBluetoothSDKListenerHelper
: который выполняетBroadcastReceiver
и запускает функцииIBluetoothSDKListener
IBluetoothSDKListener
: наш Interface, который определяет функции обратного вызоваBluetoothUtils
: который содержит имена действий, определенных для фильтрации событий вBroadcastReceiver
1) Определите действия
Первым шагом является определение файла BluetoothUtils.kt , который содержит действия, о которых мы хотим получать уведомления в нашей активности:
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"
}
}
Я определил несколько, но вы можете добавлять их по своему усмотрению.
2) Определите события-функции обратного вызова
Второй шаг - это определение нашего интерфейса, который будет содержать события, соответствующие действиям, которые мы определили в первом шаге. Итак, давайте продолжим и определим IBluetoothSDKListener
как:
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()
}
Этот интерфейс будет позже реализован в нашей активности, или фрагменте, который будет выполнять некоторые действия при появлении события. Например, когда устройство подключается, срабатывает функция onDeviceDiscovered
, и затем вы можете перейти к выполнению определенных операций, например, как мы увидим в следующих шагах, отправить сообщение по bluetooth на только что подключенное устройство через наш BluetoothSDKService
.
3) Определение BroadcastReceiver
Следующим шагом будет определение нашего BroadcastReceiver
, задачей которого будет фильтрация намерений с нашими действиями, определенными до получения LocalBroadcastManager
, для запуска функций обратного вызова, определенных в предыдущем разделе. Поэтому мы используем BluetoothSDKListenerHelper
как:
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
}
}
}
}
}
В действии или фрагменте мы реализуем наш IBluetoothSDKListener
, который мы зарегистрируем через две функции registerBluetoothSDKListner()
и unregisterBluetoothSDKListner()
. Например:
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)
}
}
Теперь наш фрагмент может быть запущен для событий, полученных BroadcastListener
, который передает их через обратные вызовы в интерфейс нашего фрагмента. Чего теперь не хватает? Ну, важная часть: сервис Bluetooth!
4) Определите сервис Bluetooth
А теперь самая сложная часть - Bluetooth Service. Мы собираемся определить класс, расширяющий Service
, в котором мы определим функции, позволяющие привязывать Service и управлять потоками Bluetooth-соединения:
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)
}
}
Чтобы сделать суть более читабельной, я закомментировал части о потоках, которые вы можете получить из официальной документации.
Как вы видите, в LocalBinder
можно определить функции, которые будут видны действиям после привязки к ним. Например, мы можем определить функции для операций обнаружения, отправки сообщения или соединения, которые затем будут выполняться операции внутри сервиса.
/**
* 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
}
Затем в потоках, управляющих сокетами, вы можете использовать функцию pushBroadcastMessage()
для генерации событий и добавления информационного наполнения, такого как удаленное устройство и сообщение. Например:
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"
)
}
}
}
Мы закончили!
Заключение
Мы видели, как из нашей активности можем связать сервис Bluetooth (1), который выполняет и управляет операциями Bluetooth. В нем мы можем запускать многоадресное событие (broadcast event) (2), которые получает Bluetooth-приемник. Получив их, Bluetooth-приемник, в свою очередь, вызывает функцию интерфейса, реализованную (4) в нашей активности, зарегистрированной на bluetooth-приемник(3)
Мой совет - всегда следовать официальному руководству и рекомендациям по написанию чистого кода.
Материал подготовлен в рамках специализации «Android Developer».
Всех желающих приглашаем на двухдневный онлайн-интенсив «Делаем мобильную мини-игру за 2 дня». За 2 дня вы сделаете мобильную версию PopIt на языке Kotlin. В приложении будет простая анимация, звук хлопка, вибрация, таймер как соревновательный элемент. Интенсив подойдет для тех, кто хочет попробовать себя в роли Android-разработчика. >> РЕГИСТРАЦИЯ
Комментарии (5)
goodvin1709
03.09.2021 00:24+2Я считаю, что описание классов должно быть в другой последовательности, сначала описание сервиса, а потом описание фрагмента где есть работа с этим сервисом.
Ну и перевод Android-specific вещей очень путает:
1. "когда вы переходите от одного действия к другому." - переход между экранами.
2. " слушатель обратного вызова". - callback.
3. "фильтрация намерений с нашими действиями." - фильтрация broadcast сообщений.
Да и материал довольно таки устарел с приходом таких библиотек и решейний как kotlin caroutines, rxjava...
FirsofMaxim
06.09.2021 07:57Bluetooth сделан адско в Android, ситуация меняется от версии к версии (улучшается), но принципы остаются примерно постоянные:
работаем последовательно (глобальная очередь задач)
используем небольшие задержки между принципиальными фазами (connecting, bonding, read/write characteristics)
для избежания блокировки main-thread, лучше все работу организовать на отдельном потоке (пуле потоков)
закладывать в код, ситуации обрыва подключения и его восстановления
AllexIn
"слушатель обртаного вызова"...
Не надо переводить колбэк. Это стандартное слово уже. Как вариант оставить callback. Но уж точно не "слушатель обратного вызова".
Наверно я бы и слушателя не стал переводить. Потому что когда пишем в тексте listener - идет пярмая ассоцияция с кодом, в котором тоже написано Listener.
Beanut
Да весь перевод - сплошные косяки. Знаете как в оригинале было "переходите от одного действия к другому"? move between different activities and fragments. Какие "активити", какие "фрагменты" - "Действия"! И это первый абзац.