feat(ble): unbondSmart() · 삭제 시 자동 재연결 + 오프라인 경고 팝업 (cloud-mvp 1982399 이식)
This commit is contained in:
@@ -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 에서 골라 호출.
|
||||
|
||||
@@ -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>(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
|
||||
|
||||
@@ -147,6 +147,13 @@
|
||||
<string name="unpair_title">페어링 해제</string>
|
||||
<string name="unpair_message">%s 의 페어링을 해제합니다. 다시 사용하려면 PIN 재입력이 필요합니다.</string>
|
||||
<string name="unpair_confirm">해제</string>
|
||||
<!-- 2026-08-04 · BLE bonding 대칭성 fix (unbondSmart flow) -->
|
||||
<string name="unpair_reconnecting_title">기기 재연결 중…</string>
|
||||
<string name="unpair_reconnecting_desc">기기에 접속해 양쪽 페어링 정보를 함께 삭제합니다. 이 과정 없이 삭제하면 다음 페어링 시 기기 초기화가 필요할 수 있습니다.</string>
|
||||
<string name="unpair_offline_title">기기가 응답하지 않습니다</string>
|
||||
<string name="unpair_offline_desc">기기가 오프라인 상태입니다. 여기서 삭제하면 이 폰의 페어링 정보만 지워지며, 다음 페어링 시 기기 초기화가 필요합니다 (전원 15초 롱프레스).\n\n선택하세요:</string>
|
||||
<string name="unpair_force_delete">강제 삭제</string>
|
||||
<string name="unpair_retry_connect">기기 켜고 다시 시도</string>
|
||||
|
||||
<!-- Reminder Notifications -->
|
||||
<string name="notification_channel_name">배뇨 알림</string>
|
||||
|
||||
@@ -147,6 +147,13 @@
|
||||
<string name="unpair_title">Unpair Device</string>
|
||||
<string name="unpair_message">Unpair %s. PIN re-entry will be required to use it again.</string>
|
||||
<string name="unpair_confirm">Unpair</string>
|
||||
<!-- 2026-08-04 · BLE bonding 대칭성 fix (unbondSmart flow) -->
|
||||
<string name="unpair_reconnecting_title">Reconnecting to device…</string>
|
||||
<string name="unpair_reconnecting_desc">Contacting the device to clear pairing on both sides. This ensures re-pairing works without device reset.</string>
|
||||
<string name="unpair_offline_title">Device is offline</string>
|
||||
<string name="unpair_offline_desc">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:</string>
|
||||
<string name="unpair_force_delete">Force delete</string>
|
||||
<string name="unpair_retry_connect">Turn on device & retry</string>
|
||||
|
||||
<!-- Reminder Notifications -->
|
||||
<string name="notification_channel_name">Voiding reminder</string>
|
||||
|
||||
Reference in New Issue
Block a user