Реализация Bluetooth как сервиса в Android
Реализация Bluetooth как сервиса в Android

Почему? И как?

Вы когда-нибудь задавали себе вопрос, прочитав официальное руководство по 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)


  1. AllexIn
    31.08.2021 18:12
    +2

    "слушатель обртаного вызова"...

    Не надо переводить колбэк. Это стандартное слово уже. Как вариант оставить callback. Но уж точно не "слушатель обратного вызова".

    Наверно я бы и слушателя не стал переводить. Потому что когда пишем в тексте listener - идет пярмая ассоцияция с кодом, в котором тоже написано Listener.


    1. Beanut
      02.09.2021 18:24
      +2

      Да весь перевод - сплошные косяки. Знаете как в оригинале было "переходите от одного действия к другому"? move between different activities and fragments. Какие "активити", какие "фрагменты" - "Действия"! И это первый абзац.


  1. goodvin1709
    03.09.2021 00:24
    +2

    Я считаю, что описание классов должно быть в другой последовательности, сначала описание сервиса, а потом описание фрагмента где есть работа с этим сервисом.
    Ну и перевод Android-specific вещей очень путает:
    1. "когда вы переходите от одного действия к другому." - переход между экранами.
    2. " слушатель обратного вызова". - callback.
    3. "фильтрация намерений с нашими действиями." - фильтрация broadcast сообщений.
    Да и материал довольно таки устарел с приходом таких библиотек и решейний как kotlin caroutines, rxjava...


  1. Terranz
    04.09.2021 22:23

    Ок хорошо, а как тут сделать соединение с последовательным портом (RFCOMM)?


  1. FirsofMaxim
    06.09.2021 07:57

    Bluetooth сделан адско в Android, ситуация меняется от версии к версии (улучшается), но принципы остаются примерно постоянные:

    1. работаем последовательно (глобальная очередь задач)

    2. используем небольшие задержки между принципиальными фазами (connecting, bonding, read/write characteristics)

    3. для избежания блокировки main-thread, лучше все работу организовать на отдельном потоке (пуле потоков)

    4. закладывать в код, ситуации обрыва подключения и его восстановления