diff --git a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt index f6c30ee..446ff98 100644 --- a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt +++ b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt @@ -410,6 +410,60 @@ class BleManager private constructor(private val context: Context) { debugLogger.info("DEVICE_UNREGISTERED $address") } + /** + * 스마트 삭제 (2026-08-04): 미연결 상태에서 삭제 시 기기 재연결 후 msr? 전송. + * + * 배경: 기기가 오프라인일 때 unbondAndRemoveAddress() 는 폰 측 LTK 만 삭제 → + * 기기는 여전히 이전 LTK 를 기대 · 재페어링 시 PIN 불일치 · 사용자가 기기 + * 15초 롱프레스로 강제 초기화해야만 재페어링 가능. + * + * 이 함수는: + * - 이미 연결됨 → disconnectAndUnbond() (msr? + 양쪽 removeBond) → onSuccess + * - 미연결 → timeoutMs 재연결 시도 + * 성공 → disconnectAndUnbond() → onSuccess + * 실패 → onDeviceOffline() (UI 가 강제 삭제 결정) + */ + fun unbondSmart( + address: String, + timeoutMs: Long = 5_000L, + onSuccess: () -> Unit, + onDeviceOffline: () -> Unit, + ) { + if (bluetoothGatt?.device?.address == address && isServiceReady.value) { + disconnectAndUnbond() + onSuccess() + return + } + debugLogger.info("UNBOND_SMART attempting reconnect for $address (timeout=${timeoutMs}ms)") + + val adapter = bluetoothAdapter ?: run { onDeviceOffline(); return } + val device = runCatching { adapter.getRemoteDevice(address) }.getOrNull() + if (device == null) { onDeviceOffline(); return } + + val timeoutRunnable = Runnable { + debugLogger.warn("UNBOND_SMART timeout · reconnect failed for $address") + try { bluetoothGatt?.disconnect(); bluetoothGatt?.close() } catch (_: Exception) {} + bluetoothGatt = null + isConnecting.value = false + onDeviceOffline() + } + + val originalCallback = onConnectionStateChanged + onConnectionStateChanged = { connected -> + originalCallback?.invoke(connected) + if (connected && bluetoothGatt?.device?.address == address) { + handler.removeCallbacks(timeoutRunnable) + onConnectionStateChanged = originalCallback + debugLogger.info("UNBOND_SMART reconnected · sending msr? for full unbond") + disconnectAndUnbond() + onSuccess() + } + } + + handler.postDelayed(timeoutRunnable, timeoutMs) + connectByAddress(address, address) + } + /** * 저장된 기기 주소로 직접 연결. 스캔 결과에 안 잡혀도 OS bond 가 있으면 바로 됨. * KnownDeviceStore 에서 골라 호출. diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt index 53e86d1..f5f6d95 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt @@ -47,6 +47,9 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +/** 저장된 기기 삭제 다이얼로그 상태 (2026-08-04 · BLE bonding 대칭성 fix). */ +private enum class UnpairPhase { Confirm, Reconnecting, OfflineWarning } + @Composable fun DeviceScanView(appState: AppState) { val context = LocalContext.current @@ -375,23 +378,57 @@ fun DeviceScanView(appState: AppState) { } } - // Delete Known Device 확인 다이얼로그 + // Delete Known Device — 2단계 다이얼로그 (2026-08-04 · BLE bonding 대칭성 fix). + var unpairPhase by remember { mutableStateOf(UnpairPhase.Confirm) } pendingDelete?.let { target -> - AlertDialog( - onDismissRequest = { pendingDelete = null }, - title = { Text(stringResource(R.string.unpair_title)) }, - text = { Text(stringResource(R.string.unpair_message, target.name)) }, - confirmButton = { - TextButton(onClick = { - bleManager.unbondAndRemoveAddress(target.address) - knownDevices = KnownDeviceStore.getAll(context) - pendingDelete = null - }) { Text(stringResource(R.string.unpair_confirm), color = Color.Red) } - }, - dismissButton = { - TextButton(onClick = { pendingDelete = null }) { Text(stringResource(R.string.cancel)) } - } - ) + when (unpairPhase) { + UnpairPhase.Confirm -> AlertDialog( + onDismissRequest = { pendingDelete = null; unpairPhase = UnpairPhase.Confirm }, + title = { Text(stringResource(R.string.unpair_title)) }, + text = { Text(stringResource(R.string.unpair_message, target.name)) }, + confirmButton = { + TextButton(onClick = { + unpairPhase = UnpairPhase.Reconnecting + bleManager.unbondSmart( + address = target.address, + onSuccess = { + knownDevices = KnownDeviceStore.getAll(context) + pendingDelete = null + unpairPhase = UnpairPhase.Confirm + }, + onDeviceOffline = { unpairPhase = UnpairPhase.OfflineWarning }, + ) + }) { Text(stringResource(R.string.unpair_confirm), color = Color.Red) } + }, + dismissButton = { + TextButton(onClick = { pendingDelete = null }) { Text(stringResource(R.string.cancel)) } + } + ) + UnpairPhase.Reconnecting -> AlertDialog( + onDismissRequest = { /* 대기 필수 */ }, + title = { Text(stringResource(R.string.unpair_reconnecting_title)) }, + text = { Text(stringResource(R.string.unpair_reconnecting_desc)) }, + confirmButton = { }, + ) + UnpairPhase.OfflineWarning -> AlertDialog( + onDismissRequest = { pendingDelete = null; unpairPhase = UnpairPhase.Confirm }, + title = { Text(stringResource(R.string.unpair_offline_title)) }, + text = { Text(stringResource(R.string.unpair_offline_desc)) }, + confirmButton = { + TextButton(onClick = { + bleManager.unbondAndRemoveAddress(target.address) + knownDevices = KnownDeviceStore.getAll(context) + pendingDelete = null + unpairPhase = UnpairPhase.Confirm + }) { Text(stringResource(R.string.unpair_force_delete), color = Color.Red) } + }, + dismissButton = { + TextButton(onClick = { + unpairPhase = UnpairPhase.Confirm + }) { Text(stringResource(R.string.unpair_retry_connect)) } + } + ) + } } // Scan button diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 52ad2e2..fed94fe 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -147,6 +147,13 @@ 페어링 해제 %s 의 페어링을 해제합니다. 다시 사용하려면 PIN 재입력이 필요합니다. 해제 + + 기기 재연결 중… + 기기에 접속해 양쪽 페어링 정보를 함께 삭제합니다. 이 과정 없이 삭제하면 다음 페어링 시 기기 초기화가 필요할 수 있습니다. + 기기가 응답하지 않습니다 + 기기가 오프라인 상태입니다. 여기서 삭제하면 이 폰의 페어링 정보만 지워지며, 다음 페어링 시 기기 초기화가 필요합니다 (전원 15초 롱프레스).\n\n선택하세요: + 강제 삭제 + 기기 켜고 다시 시도 배뇨 알림 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f936cf9..7f1edef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -147,6 +147,13 @@ Unpair Device Unpair %s. PIN re-entry will be required to use it again. Unpair + + Reconnecting to device… + Contacting the device to clear pairing on both sides. This ensures re-pairing works without device reset. + Device is offline + The device did not respond. Deleting here will remove pairing on this phone only. To pair again, the device will need to be reset (long-press power button for 15 seconds).\n\nChoose an action: + Force delete + Turn on device & retry Voiding reminder