Handle request bluetooth permission

This commit is contained in:
2026-05-25 10:18:56 +07:00
parent 66fe3be32f
commit 31ea6be8cd
14 changed files with 426 additions and 9 deletions
@@ -5,10 +5,13 @@ import app.cash.sqldelight.driver.native.NativeSqliteDriver
import com.digitoolsolutions.app.torquevaultkmp.AppStoragePlatform
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase
import com.digitoolsolutions.app.torquevaultkmp.utils.BluetoothManager
import com.digitoolsolutions.app.torquevaultkmp.utils.IosBluetoothManager
import org.koin.dsl.module
actual val platformModule = module {
single<AppStorage> { AppStoragePlatform() }
single<BluetoothManager> { IosBluetoothManager() }
single<SqlDriver> {
NativeSqliteDriver(
schema = AppDatabase.Schema,
@@ -0,0 +1,90 @@
package com.digitoolsolutions.app.torquevaultkmp.utils
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import platform.CoreBluetooth.*
import platform.Foundation.NSURL
import platform.UIKit.*
import platform.darwin.NSObject
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue
class IosBluetoothManager : BluetoothManager {
private val _isBluetoothEnabled = MutableStateFlow(false)
override val isBluetoothEnabled: StateFlow<Boolean> = _isBluetoothEnabled.asStateFlow()
private val _hasBluetoothPermission = MutableStateFlow(false)
override val hasBluetoothPermission: StateFlow<Boolean> = _hasBluetoothPermission.asStateFlow()
// Strong reference to delegate to prevent garbage collection
private val centralManagerDelegate = object : NSObject(), CBCentralManagerDelegateProtocol {
override fun centralManagerDidUpdateState(central: CBCentralManager) {
_isBluetoothEnabled.value = central.state == CBCentralManagerStatePoweredOn
checkBluetoothPermission()
}
}
private val centralManager: CBCentralManager by lazy {
val options = mapOf<Any?, Any?>(CBCentralManagerOptionShowPowerAlertKey to true)
CBCentralManager(delegate = centralManagerDelegate, queue = null, options = options)
}
init {
// Trigger lazy initialization
checkBluetoothState()
checkBluetoothPermission()
}
override fun checkBluetoothState() {
_isBluetoothEnabled.value = centralManager.state == CBCentralManagerStatePoweredOn
}
override fun checkBluetoothPermission() {
val auth = CBManager.authorization
_hasBluetoothPermission.value = auth == CBManagerAuthorizationAllowedAlways
}
override fun openSettings() {
val url = NSURL.URLWithString(UIApplicationOpenSettingsURLString)
if (url != null) {
dispatch_async(dispatch_get_main_queue()) {
if (UIApplication.sharedApplication.canOpenURL(url)) {
UIApplication.sharedApplication.openURL(url, mapOf<Any?, Any?>(), null)
}
}
}
}
override fun enableBluetooth() {
val auth = CBManager.authorization
if (auth == CBManagerAuthorizationDenied || auth == CBManagerAuthorizationRestricted) {
openSettings()
return
}
val state = centralManager.state
if (state == CBCentralManagerStatePoweredOff) {
// Attempt to open Bluetooth settings directly via App-Prefs (best effort)
val bluetoothUrl = NSURL.URLWithString("App-Prefs:root=Bluetooth")
dispatch_async(dispatch_get_main_queue()) {
val app = UIApplication.sharedApplication
if (bluetoothUrl != null && app.canOpenURL(bluetoothUrl)) {
app.openURL(bluetoothUrl, mapOf<Any?, Any?>(), null)
} else {
// Fallback: Trigger the system's "Bluetooth is Off" power alert
// This alert has a "Settings" button that goes to the right place.
centralManager.scanForPeripheralsWithServices(null, null)
centralManager.stopScan()
}
}
} else if (auth == CBManagerAuthorizationNotDetermined) {
// Accessing the state or scanning will trigger the system prompt
centralManager.scanForPeripheralsWithServices(null, null)
centralManager.stopScan()
checkBluetoothState()
}
}
}
actual fun createBluetoothManager(): BluetoothManager = IosBluetoothManager()
@@ -0,0 +1,22 @@
package com.digitoolsolutions.app.torquevaultkmp.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import org.koin.compose.koinInject
@Composable
actual fun HandlePermissionRequest(
onPermissionResult: (Boolean) -> Unit
) {
val bluetoothManager: BluetoothManager = koinInject()
LaunchedEffect(Unit) {
// Trigger enableBluetooth which on iOS will touch the CBCentralManager
// and show the permission prompt if it's currently "Not Determined"
bluetoothManager.enableBluetooth()
// We call the result immediately, but the StateFlows in BluetoothManager
// will update asynchronously via the CBCentralManagerDelegate
onPermissionResult(bluetoothManager.hasBluetoothPermission.value)
}
}