#android #service #bluetooth-lowenergy
#Android #Обслуживание #bluetooth-lowenergy
Вопрос:
Я внедряю службу, которая использует autoconnect
функцию bluetoothGatt
для подключения к устройству и мониторинга его во время подключения.
Я работаю в предположении, что устройство уже подключено (за эту часть отвечает сотрудник), поэтому с автоматическим подключением не должно возникнуть никаких проблем
мой код выглядит следующим образом:
//the callback is for the class I have created that actually does the connection
class BTService: Service(), CoroutineScope, BTConnection.Callback {
private val btReceiver by lazy { BluetoothStateReceiver(this::btStateChange) } //receiver for bt adapter changes
private var connection:BTConnection? = null
private var readJob:Job? = null
override fun onCreate() {
buildNotificationChannels()
registerReceiver(btReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)) //since I can't register this receiver in AndroidManifest any more I did it here
}
private fun btStateChange(enabled: Boolean) {
if (enabled)
startConnecting()
else
stopConnection()
}
private fun startConnecting() {
val address = prefs.address //get the current saved address
val current = connection //get the current connection
//try to stop the current connection if it is different than the one we want to set up
if (current != null amp;amp; !current.address.equals(address, true))
current.stop()
if (address.isNullOrBlank())
return
//then we create a new connection if needed
val new = if (current == null || !current.address.equals(address, true)) {
Injections.buildConnection(application, address, this)
} else {
current
}
connection = new
new.connect()
}
//this is one of the callbacks from BTConnection.Callback
override fun connected(address: String) {
if (address != connection?.address) return
val cn = connection ?: return
showConnectionNotification()
val notification = buildForegroundNotification()
startForeground(FOREGROUND_ID, notification)
readJob?.cancel()
readJob = launch {
cn.dataFlow //this is a flow that will be emmitting read data
.cancellable()
.flowOn(Dispatchers.IO)
.buffer()
.onEach(this@BTService::parseData)
.flowOn(Dispatchers.Default)
}
}
private suspend fun parseData(bytes:ByteArray) { //this is where the parsing and storage etc happens
}
private fun stopConnection() {
val cn = connection
connection = null
cn?.stop()
}
override fun disconnected(address: String) { //another callback from the connection class
showDisconnectNotification()
stopForeground(true)
}
мой код, который останавливает соединение
fun stop() {
canceled = true
if (connected)
gatt?.disconnect()
launch(Dispatchers.IO) {
delay(1000)
gatt?.close()
gatt = null
}
}
мой код основан (и зависит) от этой действительно хорошей статьи, которую я прочитал:
https://medium.com/@martijn.van.welie/making-android-ble-work-part-2-47a3cdaade07
Я также создал приемник для событий загрузки, которые будут вызывать
context.startService(Intent(context, BTService::class.java))
просто чтобы убедиться, что служба создана хотя бы один раз и приемник bt зарегистрирован
мои вопросы:
а) есть ли вероятность, что моя служба будет уничтожена, пока она не находится в режиме переднего плана? т.Е. Когда устройства нет рядом, а bluetoothGat.connect приостанавливается при автоматическом подключении? достаточно ли мне вернуть START_STICKY из onStartCommand(), чтобы убедиться, что даже когда моя служба будет уничтожена, она запустится снова?
б) если есть такой случай, есть ли способ хотя бы воссоздать службу, чтобы btReceiver был хотя бы зарегистрирован?
c) когда следует close()
вызывать BluetoothGatt в случае autoconnect = true? только при создании нового соединения (в моем примере, где я вызываю Injections.buildConnection)? должен ли я также вызывать ее, когда bluetoothadapter отключен? или я могу повторно использовать одно и то же соединение и BluetoothGatt, если пользователь выключит и снова включит адаптер Bluetooth?
d) есть ли способ узнать, не удалось ли выполнить автоматическое подключение, и не будет ли повторена попытка? и есть ли способ фактически протестировать и воспроизвести такой эффект? в упомянутой выше статье говорится, что это может произойти, когда батареи периферийного устройства почти разряжены или когда вы находитесь на границе диапазона Bluetooth
заранее спасибо за любую помощь, которую вы можете предоставить
Ответ №1:
a-b) Если в вашем приложении нет действия или службы, которая находится на переднем плане, система может отключить его в любое время. Ожидающие или активные подключения к BLE не влияют на точку зрения системы, когда нужно отключить приложение. (Когда дело доходит до сканирования рекламы, история совершенно иная.)
Общий подход к обеспечению того, чтобы автосоединения оставались в силе, заключается в том, чтобы служба переднего плана работала постоянно. Поэтому не останавливайте его, пока устройство в данный момент не подключено, если вы хотите иметь ожидающее соединение. Нет смысла использовать планировщик заданий, WorkManagers и т. Д., Поскольку наличия службы переднего плана должно быть достаточно, чтобы поддерживать процесс приложения в рабочем состоянии, а ожидающие / активные соединения поддерживаются до тех пор, пока приложение работает. Приложение вообще не использует cpu% при ожидании ожидающих подключений BLE. Однако известно, что некоторые китайские производители телефонов не следуют документации Android, иногда убивая приложения, даже если у них запущены службы переднего плана.
c) Каждый объект BluetoothGatt представляет и ссылается на объект внутри процесса Bluetooth, запущенного на том же телефоне. По умолчанию система допускает в общей сложности 32 таких объекта (в прошлый раз, когда я проверял). Чтобы освободить эти драгоценные ресурсы, вы звоните close()
. Если вы забудете, у вас будет утечка, что означает, что ваше приложение или какое-либо другое приложение может не создать объект BluetoothGatt. (Однако при завершении процессов приложения их объекты BluetoothGatt автоматически закрываются). API немного странно спроектирован, что есть как disconnect
метод, так и close
метод. Но в любом случае, disconnect
метод изящно инициирует разрыв соединения, и затем вы получите onConnectionStateChange
обратный вызов, сообщающий, когда отключение завершено. Однако вы должны позвонить close
, чтобы освободить ресурс, или позвонить connect
, если хотите повторно подключиться, или вы можете предпринять действие чуть позже. Вызов close
подключенного объекта BluetoothGatt также отключится, но вы не получите никакого обратного вызова из-за одновременного уничтожения объекта.
Поскольку все объекты BluetoothGatt представляют объекты в процессе Bluetooth, они «умрут» или перестанут работать при отключении Bluetooth, поскольку это предполагает завершение процесса Bluetooth. Это означает, что вам необходимо воссоздать все объекты BluetoothGatt при перезапуске Bluetooth. Вы можете вызвать close
старые объекты, но это ничего не даст, поскольку они мертвы. Поскольку в документации об этом ничего не говорится, я предлагаю вам позвонить close
в любом случае, чтобы быть в безопасности, если поведение изменится в будущем.
d) Чтобы определить, завершается ли connectGatt
вызов неудачно и не повторите попытку, вы можете прослушать onConnectionStateChange
обратный вызов. Если при этом выдается код ошибки, например 257, это обычно означает, что система достигла максимального количества подключений или максимального количества какого-либо ресурса. Вы можете проверить это, просто инициировав ожидающие подключения к нескольким адресам разных устройств Bluetooth.
Я бы не стал доверять утверждению о том, что новые попытки подключения будут прерваны, если периферийное устройство разряжено или находится на «границе диапазона Bluetooth». Я был бы рад увидеть ссылку на исходный код Bluetooth Android, где это происходит, поскольку я действительно считаю, что это совсем не так.
Ответ №2:
Прежде всего, если вы собираетесь распространять свое приложение в Google Play Store, вам необходимо настроить минимальный уровень API 29, если я не ошибаюсь, следовательно, вы должны использовать либо JobService, либо JobScheduler, либо WorkManager вместо Service. Это делается для поддержки фоновых ограничений, начиная с Oreo (26).
a) если вы правильно реализуете любой из двух вариантов, о которых я упоминал выше, вы можете написать надлежащую службу, которая не завершится, если вы ее не остановите. Вот некоторые ресурсы в JobService: (resource1, resource2, resource3)
б) Вы можете повторно зарегистрироваться по своему усмотрению с помощью метода onStartJob () вашего JobService, который воссоздаст ваше приложение.
c) Каждый раз, когда вы заканчиваете работу с периферийным устройством ble, вам необходимо закрыть соединение gatt с ним. Вот фрагмент из класса BluetoothGatt
/**
* Close this Bluetooth GATT client.
*
* Application should call this method as early as possible after it is done with
* this GATT client.
*/
public void close() {
Кроме того, из javadoc-класса BluetoothAdapter вы можете видеть, что все соединения завершаются корректно, когда ble отключен.
/**
* Turn off the local Bluetooth adapteramp;mdash;do not use without explicit
* user action to turn off Bluetooth.
* <p>This gracefully shuts down all Bluetooth connections, stops Bluetooth
* system services, and powers down the underlying Bluetooth hardware.
* <p class="caution"><strong>Bluetooth should never be disabled without
* direct user consent</strong>. The {@link #disable()} method is
* provided only for applications that include a user interface for changing
* system settings, such as a "power manager" app.</p>
* <p>This is an asynchronous call: it will return immediately, and
* clients should listen for {@link #ACTION_STATE_CHANGED}
* to be notified of subsequent adapter state changes. If this call returns
* true, then the adapter state will immediately transition from {@link
* #STATE_ON} to {@link #STATE_TURNING_OFF}, and some time
* later transition to either {@link #STATE_OFF} or {@link
* #STATE_ON}. If this call returns false then there was an
* immediate problem that will prevent the adapter from being turned off -
* such as the adapter already being turned off.
*
* @return true to indicate adapter shutdown has begun, or false on immediate error
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public boolean disable() {
d) Я не уверен, какой обратный вызов будет запущен. Чтобы воспроизвести, два упомянутых вами пункта кажутся допустимыми случаями, которые нужно попробовать.
Я надеюсь, что это поможет вам усовершенствовать ваш проект!