Keep Bluetooth settings writes alive and contain Android launch failures
This commit is contained in:
1 parent
ae4f524448
commit
8dc10bb54f
2 files changed
+36
-22
No files matched your search
@@ -13,15 +13,18 @@ import com.utun.chat.ChatApplication
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.util.UUID
|
||||
|
||||
/** Только Android radio I/O. Протокол, сборка пакетов и ll_queue находятся в общем C transport. */
|
||||
/** Android radio I/O на одном HandlerThread; протокол/ll_queue принадлежат C worker.
|
||||
* Application передаёт подтверждённый снимок настроек; callbacks SDK привязаны к generation.
|
||||
* applySettings без аргумента повторяет последний снимок при SERVICE_STARTED/смене Bluetooth. */
|
||||
@Suppress("MissingPermission", "DEPRECATION")
|
||||
object BleRadio {
|
||||
private val serviceUuid = UUID.fromString("f61a9020-4abc-45ac-98e1-761df4c13811")
|
||||
private val rxUuid = UUID.fromString("f61a9021-4abc-45ac-98e1-761df4c13811")
|
||||
private val txUuid = UUID.fromString("f61a9022-4abc-45ac-98e1-761df4c13811")
|
||||
private val cccUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
||||
private val thread = HandlerThread("vibechat-ble").apply { start() }
|
||||
private val worker = Handler(thread.looper)
|
||||
private val thread by lazy { HandlerThread("vibechat-ble").apply { start() } }
|
||||
private val worker by lazy { Handler(thread.looper) }
|
||||
@Volatile private var configuredFlags: Int? = null
|
||||
private val context get() = ChatApplication.instance
|
||||
private val adapter get() = context.getSystemService(BluetoothManager::class.java).adapter
|
||||
val state = MutableStateFlow(2) // BLE_CLOSED; тексты выбирает UI.
|
||||
@@ -50,16 +53,20 @@ object BleRadio {
|
||||
else arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
fun permitted() = permissions().all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
|
||||
fun init() {
|
||||
worker // Ошибка запуска потока возвращается Application до установки callback в C.
|
||||
nativeInstall()
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(c: Context, intent: Intent) { applySettings() }
|
||||
override fun onReceive(c: Context, intent: Intent) {
|
||||
try { applySettings() } catch (e: Exception) { log("ERROR", "Bluetooth state application failed: $e") }
|
||||
}
|
||||
}
|
||||
ContextCompat.registerReceiver(context, receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED), ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
fun applySettings() {
|
||||
val flags = context.configProvider.getBleFlags()
|
||||
fun applySettings(flags: Int? = configuredFlags) {
|
||||
if (flags == null) return // Пока нет подтверждённого чтения DataStore, ничего не включать.
|
||||
configuredFlags = flags
|
||||
val effective = try { if (permitted() && adapter?.isEnabled == true) flags else 0 }
|
||||
catch (e: SecurityException) { log("WARN", "Permission revoked: ${e.message}"); 0 }
|
||||
catch (e: RuntimeException) { log("WARN", "Bluetooth state unavailable: $e"); 0 }
|
||||
if (NativeLib.isReady() && !nativeConfigure(effective)) log("WARN", "Settings could not be queued")
|
||||
if (effective == 0) state.value = 2
|
||||
}
|
||||
|
||||
+22
-15
@@ -15,30 +15,31 @@ import com.utun.chat.ChatApplication
|
||||
import com.utun.chat.R
|
||||
import com.utun.chat.data.BleRadio
|
||||
import com.utun.chat.data.LogManager
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
/** Снимок DataStore без чтения диска на main; принятые записи принадлежат Application.
|
||||
* Разрешения/включение адаптера подтверждаются Android, ошибки запуска показываются в этой вкладке. */
|
||||
@Composable
|
||||
fun BluetoothSettingsTab() {
|
||||
val app = ChatApplication.instance
|
||||
val provider = app.configProvider
|
||||
val scope = rememberCoroutineScope()
|
||||
var flags by remember { mutableStateOf(provider.getBleFlags()) }
|
||||
val settings = remember(provider) { provider.changes.observe("Bluetooth settings", provider.bluetoothSettings) }
|
||||
val confirmedFlags by settings.collectAsState(initial = null)
|
||||
val flags = confirmedFlags ?: 0
|
||||
var denied by remember { mutableStateOf(false) }
|
||||
var failed by remember { mutableStateOf(false) }
|
||||
val settingFailed by provider.changes.failed.collectAsState()
|
||||
val state by BleRadio.state.collectAsState()
|
||||
val advertisingState by BleRadio.advertisingState.collectAsState()
|
||||
fun save(value: Int) { scope.launch {
|
||||
try { provider.setBleFlags(value); flags = value; BleRadio.applySettings(); failed = false }
|
||||
catch (e: CancellationException) { throw e }
|
||||
catch (e: Exception) { failed = true; LogManager.addLog("ERROR", "discovery", "BLE settings: ${e.message}") }
|
||||
} }
|
||||
fun save(value: Int) { provider.changes.submit("Bluetooth settings") { provider.setBleFlags(value) } }
|
||||
val enable = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == android.app.Activity.RESULT_OK) { denied = false; save(flags or 1) } else denied = true
|
||||
}
|
||||
fun activate() {
|
||||
if (app.getSystemService(BluetoothManager::class.java).adapter?.isEnabled == true) { denied = false; save(flags or 1) }
|
||||
else enable.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
|
||||
try {
|
||||
failed = false
|
||||
if (app.getSystemService(BluetoothManager::class.java)?.adapter?.isEnabled == true) { denied = false; save(flags or 1) }
|
||||
else enable.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
|
||||
} catch (e: Exception) { failed = true; LogManager.addLog("ERROR", "discovery", "Bluetooth enable request failed: $e") }
|
||||
}
|
||||
val permissions = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
|
||||
if (BleRadio.permitted()) activate() else denied = true
|
||||
@@ -46,20 +47,26 @@ fun BluetoothSettingsTab() {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(stringResource(R.string.ble_enabled))
|
||||
Switch(checked = flags and 1 != 0, onCheckedChange = {
|
||||
if (!it) save(flags and 1.inv())
|
||||
else if (BleRadio.permitted()) activate() else permissions.launch(BleRadio.permissions())
|
||||
Switch(enabled = confirmedFlags != null, checked = flags and 1 != 0, onCheckedChange = {
|
||||
try {
|
||||
failed = false
|
||||
if (!it) save(flags and 1.inv())
|
||||
else if (BleRadio.permitted()) activate() else permissions.launch(BleRadio.permissions())
|
||||
} catch (e: Exception) { failed = true; LogManager.addLog("ERROR", "discovery", "Bluetooth permission request failed: $e") }
|
||||
})
|
||||
}
|
||||
listOf(2 to R.string.ble_advertise, 4 to R.string.ble_incoming).forEach { (bit, label) ->
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(stringResource(label), Modifier.weight(1f))
|
||||
Switch(checked = flags and bit != 0, onCheckedChange = { save(if (it) flags or bit else flags and bit.inv()) })
|
||||
Switch(enabled = confirmedFlags != null, checked = flags and bit != 0,
|
||||
onCheckedChange = { save(if (it) flags or bit else flags and bit.inv()) })
|
||||
}
|
||||
}
|
||||
Text(stringResource(R.string.ble_hint), style = MaterialTheme.typography.bodySmall)
|
||||
Text(stringResource(when (state) { 0 -> R.string.ble_starting; 1 -> R.string.ble_ready; else -> R.string.ble_stopped }))
|
||||
if (denied) Text(stringResource(R.string.ble_permission), color = MaterialTheme.colorScheme.error)
|
||||
if (confirmedFlags == null) LinearProgressIndicator(Modifier.fillMaxWidth())
|
||||
if (settingFailed) Text(stringResource(R.string.settings_update_failed), color = MaterialTheme.colorScheme.error)
|
||||
if (failed || (state == 1 && flags and 2 != 0 && advertisingState == 2)) Text(stringResource(R.string.ble_failed), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user