Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ import android.bluetooth.BluetoothProfile
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import network.columba.app.rns.host.ble.model.BleConstants
import network.columba.app.rns.host.ble.util.BleDispatchers
import network.columba.app.rns.host.ble.util.BleOperationQueue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -206,7 +205,7 @@ class BleGattClient(
* @return Result with Unit on success, exception on failure
*/
suspend fun connect(address: String): Result<Unit> =
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
try {
// Check if already connected
connectionsMutex.withLock {
Expand Down Expand Up @@ -297,14 +296,14 @@ class BleGattClient(
}

val connData =
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
connectionsMutex.withLock {
connections.remove(address)
}
}

if (connData != null) {
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
connData.connectionJob?.cancel()
safeGattTeardown(connData.gatt, "manual disconnect $address")
}
Expand Down Expand Up @@ -446,7 +445,7 @@ class BleGattClient(
}

// Request high connection priority for better stability and throughput
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
try {
val priorityResult =
gatt.requestConnectionPriority(
Expand All @@ -461,11 +460,11 @@ class BleGattClient(
// Small delay to let BLE stack apply parameters
delay(100)

// Discover services (post to main thread for older Android versions)
withContext(Dispatchers.Main) {
Handler(Looper.getMainLooper()).post {
gatt.discoverServices()
}
// Discover services off the main thread: on Android 13+ discoverServices()
// is a synchronous binder call that can block for seconds when the
// Bluetooth stack is congested, which caused Background ANRs.
withContext(BleDispatchers.ble) {
gatt.discoverServices()
}
}

Expand Down Expand Up @@ -523,7 +522,7 @@ class BleGattClient(
connectionsMutex.withLock {
connections.remove(address)
}
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
safeGattTeardown(gatt, "service discovery failed status=$status")
}

Expand All @@ -544,7 +543,7 @@ class BleGattClient(
connectionsMutex.withLock {
connections.remove(address)
}
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
safeGattTeardown(gatt, "Reticulum service not found")
}

Expand All @@ -568,7 +567,7 @@ class BleGattClient(
connectionsMutex.withLock {
connections.remove(address)
}
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
safeGattTeardown(gatt, "required characteristics not found")
}

Expand Down Expand Up @@ -853,7 +852,7 @@ class BleGattClient(
}

if (connData != null) {
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
safeGattTeardown(connData.gatt, "failed to enable notifications")
}
}
Expand Down Expand Up @@ -1081,7 +1080,7 @@ class BleGattClient(

val connData = connectionsMutex.withLock { connections.remove(address) }
if (connData != null) {
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
safeGattTeardown(connData.gatt, "connection timeout")
}
onConnectionFailed?.invoke(address, "Connection timeout")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat
import network.columba.app.rns.host.ble.model.BleConstants
import network.columba.app.rns.host.ble.model.BleDevice
import network.columba.app.rns.host.ble.model.BlePowerSettings
import network.columba.app.rns.host.ble.util.BleDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -144,7 +145,7 @@ class BleScanner(
* @return Result indicating success or failure
*/
suspend fun startScanning(minRssi: Int = BleConstants.MIN_RSSI_DBM): Result<Unit> {
return withContext(Dispatchers.Main) {
return withContext(BleDispatchers.ble) {
try {
// Check if already scanning
if (_isScanning.value) {
Expand Down Expand Up @@ -206,7 +207,7 @@ class BleScanner(
scanJob?.cancel()
scanJob = null

withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
if (_isScanning.value && bluetoothLeScanner != null) {
bluetoothLeScanner.stopScan(scanCallback)
_isScanning.value = false
Expand Down Expand Up @@ -248,7 +249,7 @@ class BleScanner(
* Perform a single scan.
*/
private suspend fun performScan(minRssi: Int) {
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
try {
val scanner = bluetoothLeScanner ?: return@withContext

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import android.util.Log
import androidx.core.content.ContextCompat
import network.columba.app.rns.host.ble.model.BleConstants
import network.columba.app.rns.host.ble.model.BlePowerSettings
import network.columba.app.rns.host.ble.util.BleDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -142,7 +143,7 @@ class BleAdvertiser(
* @return Result indicating success or failure
*/
suspend fun startAdvertising(deviceName: String = BleConstants.DEFAULT_DEVICE_NAME_PREFIX): Result<Unit> =
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
try {
// Check if already advertising
if (_isAdvertising.value) {
Expand Down Expand Up @@ -243,7 +244,7 @@ class BleAdvertiser(
// Stop refresh job first
stopRefreshJob()

withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
if (_isAdvertising.value && bluetoothLeAdvertiser != null) {
bluetoothLeAdvertiser.stopAdvertising(advertiseCallback)
_isAdvertising.value = false
Expand Down Expand Up @@ -366,7 +367,7 @@ class BleAdvertiser(
isRefreshing = true
try {
// Stop current advertising
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
try {
bluetoothLeAdvertiser?.stopAdvertising(advertiseCallback)
} catch (e: Exception) {
Expand All @@ -377,7 +378,7 @@ class BleAdvertiser(
delay(100) // Brief delay for cleanup

// Restart advertising
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
startAdvertisingInternal()
}
Log.d(TAG, "Advertising refreshed successfully")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import network.columba.app.rns.host.ble.model.BleConstants
import network.columba.app.rns.host.ble.util.BleDispatchers
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -262,7 +263,7 @@ class BleGattServer(
* @return Result indicating success or failure
*/
suspend fun open(): Result<Unit> =
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
try {
// Check if already open
if (_isServerOpen.value) {
Expand Down Expand Up @@ -358,7 +359,7 @@ class BleGattServer(
* Close the GATT server.
*/
suspend fun close() =
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
try {
// Clear connected centrals
centralsMutex.withLock {
Expand Down Expand Up @@ -431,7 +432,7 @@ class BleGattServer(
IllegalStateException("TX characteristic not available"),
)

return withContext(Dispatchers.Main) {
return withContext(BleDispatchers.ble) {
try {
if (!hasConnectPermission()) {
return@withContext Result.failure(
Expand Down Expand Up @@ -492,7 +493,7 @@ class BleGattServer(
* We must manually clean up state and fire the disconnect callback.
*/
suspend fun disconnectCentral(address: String) =
withContext(Dispatchers.Main) {
withContext(BleDispatchers.ble) {
try {
if (!hasConnectPermission()) {
Log.w(TAG, "Cannot disconnect central, missing permission")
Expand Down Expand Up @@ -670,7 +671,7 @@ class BleGattServer(
requestId: Int,
offset: Int,
characteristic: BluetoothGattCharacteristic,
) = withContext(Dispatchers.Main) {
) = withContext(BleDispatchers.ble) {
try {
if (!hasConnectPermission()) {
return@withContext
Expand Down Expand Up @@ -735,7 +736,7 @@ class BleGattServer(
responseNeeded: Boolean,
offset: Int,
value: ByteArray,
) = withContext(Dispatchers.Main) {
) = withContext(BleDispatchers.ble) {
try {
if (!hasConnectPermission()) {
return@withContext
Expand Down Expand Up @@ -827,7 +828,7 @@ class BleGattServer(
requestId: Int,
offset: Int,
descriptor: BluetoothGattDescriptor,
) = withContext(Dispatchers.Main) {
) = withContext(BleDispatchers.ble) {
try {
if (!hasConnectPermission()) {
return@withContext
Expand Down Expand Up @@ -869,7 +870,7 @@ class BleGattServer(
responseNeeded: Boolean,
offset: Int,
value: ByteArray,
) = withContext(Dispatchers.Main) {
) = withContext(BleDispatchers.ble) {
try {
if (!hasConnectPermission()) {
return@withContext
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package network.columba.app.rns.host.ble.util

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

/**
* Dispatchers for Android Bluetooth framework calls.
*
* Since Android 13, many framework Bluetooth APIs (`BluetoothLeScanner.startScan`/`stopScan`,
* `BluetoothGatt.discoverServices`/`requestConnectionPriority`/`disconnect`,
* `BluetoothLeAdvertiser.startAdvertising`, `BluetoothGattServer.sendResponse`, ...) are
* synchronous binder calls that block the caller via `SynchronousResultReceiver` until the
* Bluetooth process replies. When the Bluetooth stack is congested (e.g. heavy RNode GATT
* writes) these calls can block for seconds, so they must NEVER run on the main thread or the
* process gets a (Background) ANR.
*
* [ble] is a single-threaded view of [Dispatchers.IO]: it keeps the serialized, one-at-a-time
* semantics the code previously got from [Dispatchers.Main], without blocking the main looper.
*/
object BleDispatchers {
val ble: CoroutineDispatcher = Dispatchers.IO.limitedParallelism(1)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
Loading