Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 additions & 0 deletions kable-core/src/androidMain/kotlin/AndroidPeripheral.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.juul.kable
import android.Manifest
import android.Manifest.permission.BLUETOOTH_CONNECT
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothStatusCodes
import android.os.Build
Expand Down Expand Up @@ -160,4 +161,10 @@ public interface AndroidPeripheral : Peripheral {
* is negotiated.
*/
public val mtu: StateFlow<Int?>

/**
* Underlying [BluetoothDevice] that the [AndroidPeripheral] represents.
*/
@KableInternalApi
public val bluetoothDevice: BluetoothDevice
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ private const val DISCOVER_SERVICES_RETRIES = 5
private const val DEFAULT_ATT_MTU = 23
private const val ATT_MTU_HEADER_SIZE = 3

@OptIn(KableInternalApi::class)
internal class BluetoothDeviceAndroidPeripheral(
private val bluetoothDevice: BluetoothDevice,
private val _bluetoothDevice: BluetoothDevice,
private val autoConnectPredicate: () -> Boolean,
private val transport: Transport,
private val phy: Phy,
Expand All @@ -61,7 +62,7 @@ internal class BluetoothDeviceAndroidPeripheral(
private val onServicesDiscovered: ServicesDiscoveredAction,
private val logging: Logging,
private val disconnectTimeout: Duration,
) : BasePeripheral(bluetoothDevice.toString()), AndroidPeripheral {
) : BasePeripheral(_bluetoothDevice.toString()), AndroidPeripheral {

init {
onBluetoothDisabled { state ->
Expand All @@ -73,10 +74,16 @@ internal class BluetoothDeviceAndroidPeripheral(
}
}

override val bluetoothDevice: BluetoothDevice = _bluetoothDevice
get() {
displayInternalLogWarning(logging)
return field
}

private val connectAction = scope.sharedRepeatableAction(::establishConnection)

override val identifier: String = bluetoothDevice.address
private val logger = Logger(logging, "Kable/Peripheral", bluetoothDevice.toString())
override val identifier: String = _bluetoothDevice.address
private val logger = Logger(logging, "Kable/Peripheral", _bluetoothDevice.toString())

private val _state = MutableStateFlow<State>(Disconnected())
override val state = _state.asStateFlow()
Expand All @@ -96,13 +103,13 @@ internal class BluetoothDeviceAndroidPeripheral(
?: throw NotConnectedException("Connection not established, current state: ${state.value}")

override val type: Type
get() = typeFrom(bluetoothDevice.type)
get() = typeFrom(_bluetoothDevice.type)

override val address: String = requireNonZeroAddress(bluetoothDevice.address)
override val address: String = requireNonZeroAddress(_bluetoothDevice.address)

@ExperimentalApi
override val name: String?
get() = bluetoothDevice.name
get() = _bluetoothDevice.name

private suspend fun establishConnection(scope: CoroutineScope): CoroutineScope {
checkBluetoothIsSupported()
Expand All @@ -112,7 +119,7 @@ internal class BluetoothDeviceAndroidPeripheral(
_state.value = State.Connecting.Bluetooth

try {
connection.value = bluetoothDevice.connect(
connection.value = _bluetoothDevice.connect(
scope.coroutineContext,
applicationContext,
autoConnectPredicate(),
Expand Down Expand Up @@ -358,7 +365,7 @@ internal class BluetoothDeviceAndroidPeripheral(
scope.cancel("$this closed")
}

override fun toString(): String = "Peripheral(bluetoothDevice=$bluetoothDevice)"
override fun toString(): String = "Peripheral(_bluetoothDevice=$_bluetoothDevice)"
}

private val WriteType.intValue: Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public actual fun CoroutineScope.peripheral(
builderAction: PeripheralBuilderAction,
): Peripheral {
advertisement as ScanResultAndroidAdvertisement
return peripheral(advertisement.bluetoothDevice, builderAction)
return peripheral(advertisement._bluetoothDevice, builderAction)
}

@Deprecated(
Expand Down
2 changes: 1 addition & 1 deletion kable-core/src/androidMain/kotlin/Peripheral.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public actual fun Peripheral(
builderAction: PeripheralBuilderAction,
): Peripheral {
advertisement as ScanResultAndroidAdvertisement
return Peripheral(advertisement.bluetoothDevice, builderAction)
return Peripheral(advertisement._bluetoothDevice, builderAction)
}

@ExperimentalApi // Experimental while evaluating if this API introduces any footguns.
Expand Down
3 changes: 3 additions & 0 deletions kable-core/src/androidMain/kotlin/PlatformAdvertisement.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.juul.kable

import android.bluetooth.BluetoothDevice
import android.os.Parcelable

public actual interface PlatformAdvertisement : Advertisement, Parcelable {
Expand All @@ -13,4 +14,6 @@ public actual interface PlatformAdvertisement : Advertisement, Parcelable {
public val address: String
public val bondState: BondState
public val bytes: ByteArray?
@KableInternalApi
public val bluetoothDevice: BluetoothDevice
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,30 @@ import android.os.Build.VERSION_CODES
import android.os.ParcelUuid
import androidx.core.util.isNotEmpty
import com.juul.kable.PlatformAdvertisement.BondState
import com.juul.kable.logs.Logging
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import kotlin.uuid.Uuid
import kotlin.uuid.toJavaUuid
import kotlin.uuid.toKotlinUuid

@OptIn(KableInternalApi::class)
@Parcelize
internal class ScanResultAndroidAdvertisement(
private val scanResult: ScanResult,
@IgnoredOnParcel
private val logging: Logging? = null,
) : PlatformAdvertisement {

internal val bluetoothDevice: BluetoothDevice
internal val _bluetoothDevice: BluetoothDevice
get() = scanResult.device

override val bluetoothDevice: BluetoothDevice
get() {
displayInternalLogWarning(logging)
return scanResult.device
}

/** @see ScanRecord.getDeviceName */
override val name: String?
get() = scanResult.scanRecord?.deviceName
Expand All @@ -34,7 +45,7 @@ internal class ScanResultAndroidAdvertisement(
* @see BluetoothDevice.getName
*/
override val peripheralName: String?
get() = bluetoothDevice.name
get() = _bluetoothDevice.name

/**
* Returns if the peripheral is connectable. Available on Android Oreo (API 26) and newer, on older versions of
Expand All @@ -44,17 +55,17 @@ internal class ScanResultAndroidAdvertisement(
get() = if (VERSION.SDK_INT >= VERSION_CODES.O) scanResult.isConnectable else null

override val address: String
get() = bluetoothDevice.address
get() = _bluetoothDevice.address

override val identifier: Identifier
get() = bluetoothDevice.address
get() = _bluetoothDevice.address

override val bondState: BondState
get() = when (bluetoothDevice.bondState) {
get() = when (_bluetoothDevice.bondState) {
BOND_NONE -> BondState.None
BOND_BONDING -> BondState.Bonding
BOND_BONDED -> BondState.Bonded
else -> error("Unknown bond state: ${bluetoothDevice.bondState}")
else -> error("Unknown bond state: ${_bluetoothDevice.bondState}")
}

/** Returns raw bytes of the underlying scan record. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.juul.kable

import com.juul.kable.logs.Logging
import platform.CoreBluetooth.CBAdvertisementDataIsConnectable
import platform.CoreBluetooth.CBAdvertisementDataLocalNameKey
import platform.CoreBluetooth.CBAdvertisementDataManufacturerDataKey
Expand All @@ -13,14 +14,22 @@ import platform.Foundation.NSNumber
import kotlin.experimental.ExperimentalNativeApi
import kotlin.uuid.Uuid

@OptIn(KableInternalApi::class)
internal class CBPeripheralCoreBluetoothAdvertisement(
override val rssi: Int,
private val data: Map<String, Any>,
internal val cbPeripheral: CBPeripheral,
internal val _cbPeripheral: CBPeripheral,
private val logging: Logging,
) : PlatformAdvertisement {

override val cbPeripheral: CBPeripheral = _cbPeripheral
get() {
displayInternalLogWarning(logging)
return field
}

override val identifier: Identifier
get() = cbPeripheral.identifier.toUuid()
get() = _cbPeripheral.identifier.toUuid()

override val name: String?
get() = data[CBAdvertisementDataLocalNameKey] as? String
Expand All @@ -37,7 +46,7 @@ internal class CBPeripheralCoreBluetoothAdvertisement(
* https://developer.apple.com/forums/thread/72343
*/
override val peripheralName: String?
get() = cbPeripheral.name
get() = _cbPeripheral.name

/** https://developer.apple.com/documentation/corebluetooth/cbadvertisementdataisconnectable */
override val isConnectable: Boolean?
Expand All @@ -47,7 +56,8 @@ internal class CBPeripheralCoreBluetoothAdvertisement(
get() = (data[CBAdvertisementDataTxPowerLevelKey] as? NSNumber)?.intValue

override val uuids: List<Uuid>
get() = (data[CBAdvertisementDataServiceUUIDsKey] as? List<CBUUID>)?.map { it.toUuid() } ?: emptyList()
get() = (data[CBAdvertisementDataServiceUUIDsKey] as? List<CBUUID>)?.map { it.toUuid() }
?: emptyList()

override fun serviceData(uuid: Uuid): ByteArray? =
serviceDataAsNSData(uuid)?.toByteArray()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,25 @@ import kotlin.time.Duration
import platform.CoreBluetooth.CBCharacteristicWriteWithResponse as CBWithResponse
import platform.CoreBluetooth.CBCharacteristicWriteWithoutResponse as CBWithoutResponse

@OptIn(KableInternalApi::class)
internal class CBPeripheralCoreBluetoothPeripheral(
private val cbPeripheral: CBPeripheral,
private val _cbPeripheral: CBPeripheral,
observationExceptionHandler: ObservationExceptionHandler,
private val onServicesDiscovered: ServicesDiscoveredAction,
private val logging: Logging,
private val disconnectTimeout: Duration,
private val forceCharacteristicEqualityByUuid: Boolean,
) : BasePeripheral(cbPeripheral.identifier.toUuid()), CoreBluetoothPeripheral {
) : BasePeripheral(_cbPeripheral.identifier.toUuid()), CoreBluetoothPeripheral {

override val cbPeripheral: CBPeripheral = _cbPeripheral
get() {
displayInternalLogWarning(logging)
return field
}

private val central = CentralManager.Default

override val identifier: Identifier = cbPeripheral.identifier.toUuid()
override val identifier: Identifier = _cbPeripheral.identifier.toUuid()
private val logger = Logger(logging, identifier = identifier.toString())

private val _state = MutableStateFlow<State>(State.Disconnected())
Expand Down Expand Up @@ -99,7 +106,8 @@ internal class CBPeripheralCoreBluetoothPeripheral(
forceCharacteristicEqualityByUuid,
exceptionHandler = observationExceptionHandler,
)
private val canSendWriteWithoutResponse = MutableStateFlow(cbPeripheral.canSendWriteWithoutResponse)
private val canSendWriteWithoutResponse =
MutableStateFlow(_cbPeripheral.canSendWriteWithoutResponse)

private val _services = MutableStateFlow<List<PlatformDiscoveredService>?>(null)
override val services = _services.asStateFlow()
Expand All @@ -112,7 +120,7 @@ internal class CBPeripheralCoreBluetoothPeripheral(

@ExperimentalApi
override val name: String?
get() = cbPeripheral.name
get() = _cbPeripheral.name

private suspend fun establishConnection(scope: CoroutineScope): CoroutineScope {
central.checkBluetoothIsOn()
Expand All @@ -123,7 +131,7 @@ internal class CBPeripheralCoreBluetoothPeripheral(
try {
connection.value = central.connectPeripheral(
scope.coroutineContext,
cbPeripheral,
_cbPeripheral,
createPeripheralDelegate(),
_state,
_services,
Expand Down Expand Up @@ -170,13 +178,13 @@ internal class CBPeripheralCoreBluetoothPeripheral(
WithResponse -> CBCharacteristicWriteWithResponse
WithoutResponse -> CBCharacteristicWriteWithoutResponse
}
return cbPeripheral.maximumWriteValueLengthForType(type).toInt()
return _cbPeripheral.maximumWriteValueLengthForType(type).toInt()
}

@ExperimentalApi // Experimental until Web Bluetooth advertisements APIs are stable.
@Throws(CancellationException::class, IOException::class)
override suspend fun rssi(): Int = connectionOrThrow().execute<DidReadRssi> {
cbPeripheral.readRSSI()
_cbPeripheral.readRSSI()
}.rssi.intValue

private suspend fun discoverServices() {
Expand Down Expand Up @@ -209,13 +217,14 @@ internal class CBPeripheralCoreBluetoothPeripheral(
val platformCharacteristic = servicesOrThrow().obtain(characteristic, writeType.properties)
when (writeType) {
WithResponse -> connectionOrThrow().execute<DidWriteValueForCharacteristic> {
cbPeripheral.writeValue(data, platformCharacteristic, CBWithResponse)
_cbPeripheral.writeValue(data, platformCharacteristic, CBWithResponse)
}

WithoutResponse -> connectionOrThrow().guard.withLock {
if (!canSendWriteWithoutResponse.updateAndGet { cbPeripheral.canSendWriteWithoutResponse }) {
if (!canSendWriteWithoutResponse.updateAndGet { _cbPeripheral.canSendWriteWithoutResponse }) {
canSendWriteWithoutResponse.first { it }
}
central.writeValue(cbPeripheral, data, platformCharacteristic, CBWithoutResponse)
central.writeValue(_cbPeripheral, data, platformCharacteristic, CBWithoutResponse)
}
}
}
Expand All @@ -239,8 +248,13 @@ internal class CBPeripheralCoreBluetoothPeripheral(
val event = connectionOrThrow().guard.withLock {
observers
.characteristicChanges
.onSubscription { central.readValue(cbPeripheral, platformCharacteristic) }
.first { event -> event.isAssociatedWith(characteristic, forceCharacteristicEqualityByUuid) }
.onSubscription { central.readValue(_cbPeripheral, platformCharacteristic) }
.first { event ->
event.isAssociatedWith(
characteristic,
forceCharacteristicEqualityByUuid,
)
}
}

return when (event) {
Expand Down Expand Up @@ -361,7 +375,7 @@ internal class CBPeripheralCoreBluetoothPeripheral(
private fun onStateChanged(action: (State) -> Unit) {
central.delegate
.connectionEvents
.filter { event -> event.identifier == cbPeripheral.identifier }
.filter { event -> event.identifier == _cbPeripheral.identifier }
.map(ConnectionEvent::toState)
.onEach(action)
.launchIn(scope)
Expand All @@ -378,22 +392,22 @@ internal class CBPeripheralCoreBluetoothPeripheral(
canSendWriteWithoutResponse,
observers.characteristicChanges,
logging,
cbPeripheral.identifier.UUIDString,
_cbPeripheral.identifier.UUIDString,
)

override fun close() {
scope.cancel("$this closed")
}

override fun toString(): String = "Peripheral(cbPeripheral=$cbPeripheral)"
override fun toString(): String = "Peripheral(cbPeripheral=$_cbPeripheral)"
}

private val CBDescriptor.isUnsignedShortValue: Boolean
get() = UUID.UUIDString.let {
it == CBUUIDCharacteristicExtendedPropertiesString ||
it == CBUUIDClientCharacteristicConfigurationString ||
it == CBUUIDServerCharacteristicConfigurationString ||
it == CBUUIDL2CAPPSMCharacteristicString
it == CBUUIDClientCharacteristicConfigurationString ||
it == CBUUIDServerCharacteristicConfigurationString ||
it == CBUUIDL2CAPPSMCharacteristicString
}

private val Any?.type: String?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ internal class CentralManagerCoreBluetoothScanner(
)
}
.map { (cbPeripheral, rssi, advertisementData) ->
CBPeripheralCoreBluetoothAdvertisement(rssi.intValue, advertisementData, cbPeripheral)
CBPeripheralCoreBluetoothAdvertisement(rssi.intValue, advertisementData, cbPeripheral, logging)
}
}

Expand Down
Loading