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