fix(android): repair bluetooth bond on startup

This commit is contained in:
fy59 2026-09-22 16:25:40 +02:00
parent d5b4584ae2
commit 1d8450ad1c
3 changed files with 89 additions and 8 deletions

View file

@ -14,6 +14,9 @@ import com.labfytools.trainlog.data.TrainlogRepository
import com.labfytools.trainlog.data.SyncExporter
import com.labfytools.trainlog.data.SyncCatalogInbox
import com.labfytools.trainlog.data.SyncRequestOutbox
import com.labfytools.trainlog.data.BackgroundSyncSettings
import com.labfytools.trainlog.data.SyncBackgroundService
import com.labfytools.trainlog.data.hasBluetoothConnectPermission
import com.labfytools.trainlog.ui.TrainlogApp
import com.labfytools.trainlog.ui.TrainlogAppState
import com.labfytools.trainlog.ui.LanguagePresentation
@ -30,6 +33,13 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
languageSettings = LanguageSettingsOwner(applicationContext)
if (
BackgroundSyncSettings(applicationContext).enabled &&
hasBluetoothConnectPermission(applicationContext)
) {
SyncBackgroundService.start(applicationContext)
}
val repository =
TrainlogRepository(
applicationContext

View file

@ -3,11 +3,13 @@ package com.labfytools.trainlog.data
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothClass
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
@ -355,6 +357,9 @@ internal class BluetoothSyncClient(
private val context: Context,
private val repository: TrainlogRepository,
) {
companion object {
private const val TAG = "TrainlogBluetoothSync"
}
@Volatile private var activeSocket: BluetoothSocket? = null
private val settings = BluetoothSyncSettings(context)
private val codec = BluetoothFrameCodec(context.cacheDir)
@ -374,12 +379,22 @@ internal class BluetoothSyncClient(
continue
}
val address = settings.desktopAddress ?: continue
val device =
runCatching { adapter.getRemoteDevice(address) }
.onFailure { Log.w(TAG, "Configured Bluetooth desktop is invalid", it) }
.getOrNull()
if (device == null || !ensureBond(device, stopped)) {
sleep(stopped, Duration.ofSeconds(5))
continue
}
val socket =
runCatching {
adapter
.getRemoteDevice(address)
.createRfcommSocketToServiceRecord(UUID.fromString(TRAINLOG_BLUETOOTH_SERVICE_UUID))
}.getOrNull()
device.createRfcommSocketToServiceRecord(
UUID.fromString(TRAINLOG_BLUETOOTH_SERVICE_UUID)
)
}
.onFailure { Log.w(TAG, "Unable to create RFCOMM socket", it) }
.getOrNull()
if (socket == null) {
sleep(stopped, Duration.ofSeconds(10))
continue
@ -387,10 +402,10 @@ internal class BluetoothSyncClient(
activeSocket = socket
try {
socket.connect()
Log.i(TAG, "RFCOMM connected to selected Trainlog desktop")
serve(socket, stopped)
} catch (_: Exception) {
// Arrival polling is intentionally silent. A foreground manual
// synchronization still exposes its own correlated diagnostic.
} catch (error: Exception) {
Log.w(TAG, "RFCOMM connection ended: ${error.message}", error)
} finally {
activeSocket = null
runCatching { socket.close() }
@ -404,6 +419,33 @@ internal class BluetoothSyncClient(
activeSocket = null
}
@SuppressLint("MissingPermission")
private fun ensureBond(device: BluetoothDevice, stopped: AtomicBoolean): Boolean {
if (device.bondState == BluetoothDevice.BOND_BONDED) return true
if (device.bondState == BluetoothDevice.BOND_NONE) {
Log.i(TAG, "Bluetooth bond missing; requesting one-time repair")
if (!runCatching { device.createBond() }.getOrDefault(false)) {
Log.w(TAG, "Bluetooth bond request was rejected by Android")
return false
}
}
val deadline = System.nanoTime() + Duration.ofSeconds(30).toNanos()
while (!stopped.get() && System.nanoTime() < deadline) {
when (device.bondState) {
BluetoothDevice.BOND_BONDED -> {
Log.i(TAG, "Bluetooth bond ready")
return true
}
BluetoothDevice.BOND_NONE -> {
Log.w(TAG, "Bluetooth bond repair failed")
return false
}
}
sleep(stopped, Duration.ofMillis(250))
}
return device.bondState == BluetoothDevice.BOND_BONDED
}
private fun sleep(stopped: AtomicBoolean, duration: Duration) {
val deadline = System.nanoTime() + duration.toNanos()
while (!stopped.get() && System.nanoTime() < deadline) {

View file

@ -2,7 +2,7 @@
## Status
`TRAINLOG_SYNC_BLUETOOTH_TRANSPORT_V1=SOFTWARE_VALIDATED_DEVICE_TEST_PENDING`
`TRAINLOG_SYNC_BLUETOOTH_TRANSPORT_V1=LIVE_DEVICE_PASS_DRIVE_SETUP_PENDING`
This transport is additive. It does not change generation manifests, artifact
formats, causal deletion, revision ancestry, acknowledgement semantics, SQLite
@ -30,6 +30,35 @@ desktop are within Bluetooth range.
The manual Web and Android Synchroniser actions remain as recovery/forced-sync
controls.
## Private live-device evidence — 2026-09-22
The authorized Samsung Android peer and ArchASP completed a physical Bluetooth
Classic/RFCOMM synchronization without MTP or Drive as a sync transport.
- request: `sr_86ccc07f-cc5b-4506-86c1-06c00bd8b06a`;
- run: `sy_df8f87fb-e4c1-44f5-b3f7-8fe87942d19e`;
- inbound Android generation:
`gen_9609d163-4a3a-4187-a806-84379a1bcf4c`;
- outbound desktop generation:
`gen_bc1d5e4b-4b16-49bc-b228-c3ff1f1a1705`;
- final transport: `bt`;
- final Bluetooth state: `success`;
- final phase/result: `completed/completed`;
- desktop SQLite integrity: `ok`;
- post-sync AI export: `delivered_to_android`;
- Android copy:
`Documents/Trainlog/trainlog_ai_export_v1.json`, 66,971 bytes,
SHA-256 `49f7f02fdeebd15699655e43fdb475bb20d9dd2d1bc59d2bdb4cd099ae50ecda`.
The initial legacy bond contained mismatched link-key state. The private device
was explicitly re-paired; Android then reported the bond ready and established
the Trainlog RFCOMM service. Android now also repairs a missing bond through the
normal platform pairing flow before attempting RFCOMM.
The one remaining manual setup step is selecting the Android SAF destination
`Trainlog/AI`. This is intentionally user-authorized provider access and is
not bypassed by ADB or Trainlog.
## Trust and pairing
Bluetooth device names are presentation only and are never authority.