

Почему? И как?
Вы когда-нибудь задавали себе вопрос, прочитав официальное руководство по bluetooth для Android, как управлять им внутри вашего приложения? Как сохранить соединение активным, даже когда вы переходите от одного действия к другому?
Что ж, в этом руководстве я постараюсь показать вам, как я реализовал связь bluetooth через Service, чтобы управлять bluetooth и соединением с различными действиями, используя Service Binding, а также установил слушатель обратного вызова для операций, получающих информацию о состоянии связи bluetooth.
В этом руководстве мы создадим четыре файла:
BluetoothSDKService:который реализует функциональные возможности bluetooth и выдаетLocalBroadcastсообщения во время операцийBluetoothSDKListenerHelper: который выполняетBroadcastReceiverи запускает функцииIBluetoothSDKListenerIBluetoothSDKListener: наш 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-разработчика. >> РЕГИСТРАЦИЯ
