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
92 changes: 6 additions & 86 deletions kable-core/src/androidMain/kotlin/Bluetooth.kt
Original file line number Diff line number Diff line change
@@ -1,63 +1,35 @@
package com.juul.kable

import android.bluetooth.BluetoothAdapter.ERROR
import android.bluetooth.BluetoothAdapter.EXTRA_STATE
import android.bluetooth.BluetoothAdapter.STATE_OFF
import android.bluetooth.BluetoothAdapter.STATE_ON
import android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF
import android.bluetooth.BluetoothAdapter.STATE_TURNING_ON
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.IntentFilter
import android.location.LocationManager
import android.location.LocationManager.EXTRA_PROVIDER_ENABLED
import android.location.LocationManager.PROVIDERS_CHANGED_ACTION
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.R
import androidx.core.content.ContextCompat
import androidx.core.location.LocationManagerCompat
import com.juul.kable.Bluetooth.Availability.Available
import com.juul.kable.Bluetooth.Availability.Unavailable
import com.juul.kable.Reason.AdapterNotAvailable
import com.juul.kable.Reason.LocationServicesDisabled
import com.juul.kable.Reason.Off
import com.juul.kable.Reason.TurningOff
import com.juul.kable.Reason.TurningOn
import com.juul.tuulbox.coroutines.flow.broadcastReceiverFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED as BLUETOOTH_STATE_CHANGED

@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
public actual enum class Reason {
@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
Off, // BluetoothAdapter.STATE_OFF

@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
TurningOff, // BluetoothAdapter.STATE_TURNING_OFF or BluetoothAdapter.STATE_BLE_TURNING_OFF

@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
TurningOn, // BluetoothAdapter.STATE_TURNING_ON or BluetoothAdapter.STATE_BLE_TURNING_ON

Expand All @@ -69,6 +41,7 @@ public actual enum class Reason {
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
AdapterNotAvailable,

Expand All @@ -77,60 +50,7 @@ public actual enum class Reason {
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
LocationServicesDisabled,
}

private fun Context.getLocationManagerOrNull() =
ContextCompat.getSystemService(this, LocationManager::class.java)

private fun Context.isLocationEnabledOrNull(): Boolean? =
getLocationManagerOrNull()?.let(LocationManagerCompat::isLocationEnabled)

private val locationEnabledOrNullFlow = when {
SDK_INT > R -> flowOf(true)
else -> broadcastReceiverFlow(IntentFilter(PROVIDERS_CHANGED_ACTION))
.map { intent ->
if (SDK_INT == R) {
intent.getBooleanExtra(EXTRA_PROVIDER_ENABLED, false)
} else {
applicationContext.isLocationEnabledOrNull()
}
}
.onStart { emit(applicationContext.isLocationEnabledOrNull()) }
.distinctUntilChanged()
}

private val bluetoothStateFlow = flow {
when (val adapter = getBluetoothAdapterOrNull()) {
null -> emit(Unavailable(reason = AdapterNotAvailable))
else -> emitAll(
broadcastReceiverFlow(IntentFilter(BLUETOOTH_STATE_CHANGED))
.map { intent -> intent.getIntExtra(EXTRA_STATE, ERROR) }
.onStart {
emit(if (adapter.isEnabled) STATE_ON else STATE_OFF)
}
.map { state ->
when (state) {
STATE_ON -> Available
STATE_OFF -> Unavailable(reason = Off)
STATE_TURNING_OFF -> Unavailable(reason = TurningOff)
STATE_TURNING_ON -> Unavailable(reason = TurningOn)
else -> error("Unexpected bluetooth state: $state")
}
},
)
}
}

internal actual val bluetoothAvailability: Flow<Bluetooth.Availability> =
combine(
locationEnabledOrNullFlow,
bluetoothStateFlow,
) { locationEnabled, bluetoothState ->
when (locationEnabled) {
true -> bluetoothState
false -> Unavailable(reason = LocationServicesDisabled)
null -> Unavailable(reason = AdapterNotAvailable)
}
}
37 changes: 6 additions & 31 deletions kable-core/src/appleMain/kotlin/Bluetooth.kt
Original file line number Diff line number Diff line change
@@ -1,75 +1,50 @@
package com.juul.kable

import com.juul.kable.Bluetooth.Availability.Available
import com.juul.kable.Bluetooth.Availability.Unavailable
import com.juul.kable.Reason.Off
import com.juul.kable.Reason.Resetting
import com.juul.kable.Reason.Unauthorized
import com.juul.kable.Reason.Unknown
import com.juul.kable.Reason.Unsupported
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import platform.CoreBluetooth.CBCentralManagerStatePoweredOff
import platform.CoreBluetooth.CBCentralManagerStatePoweredOn
import platform.CoreBluetooth.CBCentralManagerStateResetting
import platform.CoreBluetooth.CBCentralManagerStateUnauthorized
import platform.CoreBluetooth.CBCentralManagerStateUnsupported

/** https://developer.apple.com/documentation/corebluetooth/cbmanagerstate */
@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
public actual enum class Reason {
@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
Off, // CBManagerState.poweredOff

@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
Resetting, // CBManagerState.resetting

@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
Unauthorized, // CBManagerState.unauthorized

@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
Unsupported, // CBManagerState.unsupported

@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
Unknown, // CBManagerState.unknown
}

internal actual val bluetoothAvailability: Flow<Bluetooth.Availability> = flow {
// flow + emitAll dance so that lazy `CentralManager.Default` is not initialized until this flow is active.
emitAll(CentralManager.Default.delegate.state)
}.map { state ->
when (state) {
CBCentralManagerStatePoweredOn -> Available
CBCentralManagerStatePoweredOff -> Unavailable(reason = Off)
CBCentralManagerStateResetting -> Unavailable(reason = Resetting)
CBCentralManagerStateUnauthorized -> Unavailable(reason = Unauthorized)
CBCentralManagerStateUnsupported -> Unavailable(reason = Unsupported)
else -> Unavailable(reason = Unknown)
}
}
8 changes: 5 additions & 3 deletions kable-core/src/commonMain/kotlin/Bluetooth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ public object Bluetooth {
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
public data object Available : Availability()

@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
public data class Unavailable(val reason: Reason?) : Availability()
}
Expand All @@ -60,8 +62,10 @@ public object Bluetooth {
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
public val availability: Flow<Availability> = bluetoothAvailability
public val availability: Flow<Availability>
get() = error("Deprecated")

/**
* Checks if Bluetooth Low Energy is supported on the system. Being supported (a return of
Expand All @@ -76,5 +80,3 @@ public object Bluetooth {
@ExperimentalApi // Due to the inability to query Bluetooth support w/o showing a dialog on Apple, this function may be removed.
public suspend fun isSupported(): Boolean = isBluetoothSupported()
}

internal expect val bluetoothAvailability: Flow<Bluetooth.Availability>
34 changes: 0 additions & 34 deletions kable-core/src/jsMain/kotlin/BluetoothAvailability.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
package com.juul.kable

import com.juul.kable.Bluetooth.Availability.Available
import com.juul.kable.Bluetooth.Availability.Unavailable
import com.juul.kable.Reason.BluetoothUndefined
import com.juul.kable.external.BluetoothAvailabilityChanged
import kotlinx.coroutines.await
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.onStart
import org.w3c.dom.events.Event

@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
Expand All @@ -28,25 +16,3 @@ public actual enum class Reason {
}

private const val AVAILABILITY_CHANGED = "availabilitychanged"

internal actual val bluetoothAvailability: Flow<Bluetooth.Availability> =
bluetoothOrNull()?.let { bluetooth ->
callbackFlow {
// https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/onavailabilitychanged
val listener: (Event) -> Unit = { event ->
val isAvailable = event.unsafeCast<BluetoothAvailabilityChanged>().value
trySend(if (isAvailable) Available else Unavailable(reason = null))
}

bluetooth.apply {
addEventListener(AVAILABILITY_CHANGED, listener)
awaitClose {
removeEventListener(AVAILABILITY_CHANGED, listener)
}
}
}.onStart {
val isAvailable = bluetooth.getAvailability().await()
val availability = if (isAvailable) Available else Unavailable(reason = null)
emit(availability)
}
} ?: flowOf(Unavailable(reason = BluetoothUndefined))
12 changes: 6 additions & 6 deletions kable-core/src/jvmMain/kotlin/com/juul/kable/Bluetooth.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.juul.kable

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

@Deprecated(
message = "`Bluetooth.availability` has inconsistent behavior across platforms. " +
"Will be removed in a future release. " +
"See https://github.com/JuulLabs/kable/issues/737 for more details.",
level = DeprecationLevel.ERROR,
)
public actual enum class Reason {
// Not implemented.
}

// This is not a proper implementation, but this property is deprecated, so...
internal actual val bluetoothAvailability: Flow<Bluetooth.Availability> = flowOf(Bluetooth.Availability.Available)
Loading