Skip to content

Commit

Permalink
Add Bluetooth.isSupported() function
Browse files Browse the repository at this point in the history
  • Loading branch information
twyatt committed Aug 16, 2024
1 parent d968e40 commit 6878ab3
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 0 deletions.
1 change: 1 addition & 0 deletions kable-core/api/android/kable-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public final class com/juul/kable/AndroidPeripheral$WriteResult : java/lang/Enum
public final class com/juul/kable/Bluetooth {
public static final field INSTANCE Lcom/juul/kable/Bluetooth;
public final fun getAvailability ()Lkotlinx/coroutines/flow/Flow;
public final fun isSupported (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract class com/juul/kable/Bluetooth$Availability {
Expand Down
1 change: 1 addition & 0 deletions kable-core/api/jvm/kable-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public abstract interface class com/juul/kable/Advertisement {
public final class com/juul/kable/Bluetooth {
public static final field INSTANCE Lcom/juul/kable/Bluetooth;
public final fun getAvailability ()Lkotlinx/coroutines/flow/Flow;
public final fun isSupported (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract class com/juul/kable/Bluetooth$Availability {
Expand Down
9 changes: 9 additions & 0 deletions kable-core/src/androidMain/kotlin/bluetooth/IsSupported.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.juul.kable.bluetooth

import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
import com.juul.kable.applicationContext
import com.juul.kable.getBluetoothAdapterOrNull

internal actual suspend fun isSupported(): Boolean =
applicationContext.packageManager.hasSystemFeature(FEATURE_BLUETOOTH_LE) &&
getBluetoothAdapterOrNull() != null
49 changes: 49 additions & 0 deletions kable-core/src/appleMain/kotlin/bluetooth/IsSupported.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.juul.kable.bluetooth

import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import platform.CoreBluetooth.CBCentralManager
import platform.CoreBluetooth.CBCentralManagerDelegateProtocol
import platform.CoreBluetooth.CBCentralManagerOptionShowPowerAlertKey
import platform.CoreBluetooth.CBManagerState
import platform.CoreBluetooth.CBManagerStateResetting
import platform.CoreBluetooth.CBManagerStateUnknown
import platform.CoreBluetooth.CBManagerStateUnsupported
import platform.darwin.NSObject

// Prevent triggering permissions dialog.
// https://chrismaddern.com/determine-whether-bluetooth-is-enabled-on-ios-passively/
private val options = mapOf(CBCentralManagerOptionShowPowerAlertKey to false) as Map<Any?, *>?

private var cachedState: CBManagerState? = null
private val mutex = Mutex()

internal actual suspend fun isSupported() = mutex.withLock {
cachedState ?: awaitState().also { cachedState = it }
} != CBManagerStateUnsupported

// Need to hold strong-reference to CBCentralManager and its delegate (until no longer needed).
private var managerRef: NSObject? = null
private var delegateRef: NSObject? = null

private suspend fun awaitState() = callbackFlow {
val delegate = object : NSObject(), CBCentralManagerDelegateProtocol {
override fun centralManagerDidUpdateState(central: CBCentralManager) {
trySend(central.state).onFailure {
// Silently ignore.
}
}
}.also { delegateRef = it }
CBCentralManager(delegate, null, options).also { managerRef = it }
awaitClose {
managerRef = null
delegateRef = null
}
}.first { it.isDetermined }

private val CBManagerState.isDetermined: Boolean
get() = this != CBManagerStateUnknown && this != CBManagerStateResetting
10 changes: 10 additions & 0 deletions kable-core/src/commonMain/kotlin/Bluetooth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package com.juul.kable
import com.benasher44.uuid.Uuid
import kotlinx.coroutines.flow.Flow
import kotlin.jvm.JvmName
import com.juul.kable.bluetooth.isSupported as isBluetoothSupported

public expect enum class Reason

Expand Down Expand Up @@ -34,6 +35,15 @@ public object Bluetooth {
public data class Unavailable(val reason: Reason?) : Availability()
}

/**
* Checks if Bluetooth Low Energy is supported on the system. Being supported (a return of
* `true`) does not necessarily mean that bluetooth operations will work. The radio could be off
* of permissions may be denied.
*
* This function is idempotent.
*/
public suspend fun isSupported(): Boolean = isBluetoothSupported()

public val availability: Flow<Availability> = bluetoothAvailability
}

Expand Down
3 changes: 3 additions & 0 deletions kable-core/src/commonMain/kotlin/bluetooth/IsSupported.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.juul.kable.bluetooth

internal expect suspend fun isSupported(): Boolean
22 changes: 22 additions & 0 deletions kable-core/src/jsMain/kotlin/bluetooth/IsSupported.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.juul.kable.bluetooth

import com.juul.kable.InternalException
import com.juul.kable.bluetoothOrNull
import js.errors.JsError
import js.errors.TypeError
import kotlinx.coroutines.await

internal actual suspend fun isSupported(): Boolean {
val bluetooth = bluetoothOrNull() ?: return false
val promise = try {
bluetooth.getAvailability()
} catch (e: TypeError) {
// > TypeError: navigator.bluetooth.getAvailability is not a function
return false
}
return try {
promise.await()
} catch (e: JsError) {
throw InternalException("Failed to get bluetooth availability", e)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.juul.kable.bluetooth

import com.juul.kable.jvmNotImplementedException

internal actual suspend fun isSupported(): Boolean = jvmNotImplementedException()

0 comments on commit 6878ab3

Please sign in to comment.