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")
|
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 가 있으면 바로 됨.
|
* 저장된 기기 주소로 직접 연결. 스캔 결과에 안 잡혀도 OS bond 가 있으면 바로 됨.
|
||||||
* KnownDeviceStore 에서 골라 호출.
|
* KnownDeviceStore 에서 골라 호출.
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
/** 저장된 기기 삭제 다이얼로그 상태 (2026-08-04 · BLE bonding 대칭성 fix). */
|
||||||
|
private enum class UnpairPhase { Confirm, Reconnecting, OfflineWarning }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceScanView(appState: AppState) {
|
fun DeviceScanView(appState: AppState) {
|
||||||
val context = LocalContext.current
|
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 ->
|
pendingDelete?.let { target ->
|
||||||
AlertDialog(
|
when (unpairPhase) {
|
||||||
onDismissRequest = { pendingDelete = null },
|
UnpairPhase.Confirm -> AlertDialog(
|
||||||
title = { Text(stringResource(R.string.unpair_title)) },
|
onDismissRequest = { pendingDelete = null; unpairPhase = UnpairPhase.Confirm },
|
||||||
text = { Text(stringResource(R.string.unpair_message, target.name)) },
|
title = { Text(stringResource(R.string.unpair_title)) },
|
||||||
confirmButton = {
|
text = { Text(stringResource(R.string.unpair_message, target.name)) },
|
||||||
TextButton(onClick = {
|
confirmButton = {
|
||||||
bleManager.unbondAndRemoveAddress(target.address)
|
TextButton(onClick = {
|
||||||
knownDevices = KnownDeviceStore.getAll(context)
|
unpairPhase = UnpairPhase.Reconnecting
|
||||||
pendingDelete = null
|
bleManager.unbondSmart(
|
||||||
}) { Text(stringResource(R.string.unpair_confirm), color = Color.Red) }
|
address = target.address,
|
||||||
},
|
onSuccess = {
|
||||||
dismissButton = {
|
knownDevices = KnownDeviceStore.getAll(context)
|
||||||
TextButton(onClick = { pendingDelete = null }) { Text(stringResource(R.string.cancel)) }
|
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
|
// Scan button
|
||||||
|
|||||||
@@ -147,6 +147,13 @@
|
|||||||
<string name="unpair_title">페어링 해제</string>
|
<string name="unpair_title">페어링 해제</string>
|
||||||
<string name="unpair_message">%s 의 페어링을 해제합니다. 다시 사용하려면 PIN 재입력이 필요합니다.</string>
|
<string name="unpair_message">%s 의 페어링을 해제합니다. 다시 사용하려면 PIN 재입력이 필요합니다.</string>
|
||||||
<string name="unpair_confirm">해제</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 -->
|
<!-- Reminder Notifications -->
|
||||||
<string name="notification_channel_name">배뇨 알림</string>
|
<string name="notification_channel_name">배뇨 알림</string>
|
||||||
|
|||||||
@@ -147,6 +147,13 @@
|
|||||||
<string name="unpair_title">Unpair Device</string>
|
<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_message">Unpair %s. PIN re-entry will be required to use it again.</string>
|
||||||
<string name="unpair_confirm">Unpair</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 -->
|
<!-- Reminder Notifications -->
|
||||||
<string name="notification_channel_name">Voiding reminder</string>
|
<string name="notification_channel_name">Voiding reminder</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user